Browse Source

Merge pull request #1317 from nightscout/feat/remove-js-oref

Remove the Javascript algorithm and JSON marshaling from Trio
Deniz Cengiz 14 hours ago
parent
commit
aceb300ac3
47 changed files with 1545 additions and 1998 deletions
  1. 0 35
      Model/Helper/CarbEntryStored+helper.swift
  2. 50 149
      Model/Helper/PumpEvent+helper.swift
  3. 12 24
      Trio.xcodeproj/project.pbxproj
  4. 0 2
      Trio/Resources/javascript/bundle/autosens.js
  5. 0 1
      Trio/Resources/javascript/bundle/autotune-core.js
  6. 0 2
      Trio/Resources/javascript/bundle/autotune-prep.js
  7. 0 1
      Trio/Resources/javascript/bundle/basal-set-temp.js
  8. 0 1
      Trio/Resources/javascript/bundle/determine-basal.js
  9. 0 1
      Trio/Resources/javascript/bundle/glucose-get-last.js
  10. 0 2
      Trio/Resources/javascript/bundle/iob.js
  11. 0 2
      Trio/Resources/javascript/bundle/meal.js
  12. 0 2
      Trio/Resources/javascript/bundle/profile.js
  13. 0 5
      Trio/Resources/javascript/middleware/determine_basal.js
  14. 0 26
      Trio/Resources/javascript/prepare/autosens.js
  15. 0 17
      Trio/Resources/javascript/prepare/autotune-core.js
  16. 0 47
      Trio/Resources/javascript/prepare/autotune-prep.js
  17. 0 49
      Trio/Resources/javascript/prepare/determine-basal.js
  18. 0 15
      Trio/Resources/javascript/prepare/iob.js
  19. 0 6
      Trio/Resources/javascript/prepare/log.js
  20. 0 35
      Trio/Resources/javascript/prepare/meal.js
  21. 0 107
      Trio/Resources/javascript/prepare/profile.js
  22. 5 7
      Trio/Sources/APS/APSManager.swift
  23. 5 0
      Trio/Sources/APS/Extensions/DecimalExtensions.swift
  24. 0 27
      Trio/Sources/APS/OpenAPS/Constants.swift
  25. 0 146
      Trio/Sources/APS/OpenAPS/JavaScriptWorker.swift
  26. 201 640
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  27. 0 26
      Trio/Sources/APS/OpenAPS/Script.swift
  28. 0 70
      Trio/Sources/APS/OpenAPSSwift/JSONBridge.swift
  29. 1 1
      Trio/Sources/APS/OpenAPSSwift/Models/IobResult.swift
  30. 1 1
      Trio/Sources/APS/OpenAPSSwift/Models/Profile.swift
  31. 0 210
      Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift
  32. 0 13
      Trio/Sources/APS/OpenAPSSwift/OrefFunctionResult.swift
  33. 63 0
      Trio/Sources/APS/Storage/CarbsStorage.swift
  34. 96 0
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  35. 7 137
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  36. 0 86
      Trio/Sources/Models/AlgorithmGlucose.swift
  37. 5 0
      Trio/Sources/Models/BloodGlucose.swift
  38. 0 4
      Trio/Sources/Models/Determination.swift
  39. 0 5
      Trio/Sources/Models/TrioSettings.swift
  40. 0 3
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  41. 0 33
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  42. 0 5
      Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift
  43. 292 0
      TrioTests/CarbsNativeConversionTests.swift
  44. 461 0
      TrioTests/GlucoseNativeConversionTests.swift
  45. 13 31
      TrioTests/GlucoseSmoothingTests.swift
  46. 17 24
      TrioTests/JSONImporterTests.swift
  47. 316 0
      TrioTests/PumpHistoryNativeConversionTests.swift

+ 0 - 35
Model/Helper/CarbEntryStored+helper.swift

@@ -69,38 +69,3 @@ extension CarbEntryStored {
         return request
     }
 }
-
-extension CarbEntryStored: Encodable {
-    enum CodingKeys: String, CodingKey {
-        case actualDate
-        case created_at
-        case carbs
-        case fat
-        case id
-        case isFPU
-        case note
-        case protein
-        case enteredBy
-    }
-
-    public func encode(to encoder: Encoder) throws {
-        var container = encoder.container(keyedBy: CodingKeys.self)
-
-        let dateFormatter = ISO8601DateFormatter()
-        dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-
-        let formattedDate = dateFormatter.string(from: date ?? Date())
-        try container.encode(formattedDate, forKey: .actualDate)
-        try container.encode(formattedDate, forKey: .created_at)
-
-        // TODO: handle this conditionally; pass in the enteredBy string (manual entry or via NS or Apple Health)
-        try container.encode("Trio", forKey: .enteredBy)
-
-        try container.encode(carbs, forKey: .carbs)
-        try container.encode(fat, forKey: .fat)
-        try container.encode(isFPU, forKey: .isFPU)
-        try container.encode(note, forKey: .note)
-        try container.encode(protein, forKey: .protein)
-        try container.encode(id, forKey: .id)
-    }
-}

+ 50 - 149
Model/Helper/PumpEvent+helper.swift

@@ -134,189 +134,90 @@ extension NSPredicate {
     }
 }
 
-// Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
-struct BolusDTO: Codable {
-    var id: String
-    var timestamp: String
-    var amount: Double
-    var isExternal: Bool
-    var isSMB: Bool
-    var duration: Int
-    var _type: String = EventType.bolus.rawValue
-}
-
-struct TempBasalDTO: Codable {
-    var id: String
-    var timestamp: String
-    var temp: String
-    var rate: Double
-    var _type: String = EventType.tempBasal.rawValue
-}
-
-struct TempBasalDurationDTO: Codable {
-    var id: String
-    var timestamp: String
-    var duration: Int
-    var _type: String = EventType.tempBasalDuration.rawValue
-
-    private enum CodingKeys: String, CodingKey {
-        case id
-        case timestamp
-        case duration = "duration (min)"
-        case _type
-    }
-}
-
-struct SuspendDTO: Codable {
-    var id: String
-    var timestamp: String
-    var _type: String = EventType.pumpSuspend.rawValue
-}
+// MARK: - Native mapping
 
-struct ResumeDTO: Codable {
-    var id: String
-    var timestamp: String
-    var _type: String = EventType.pumpResume.rawValue
-}
-
-struct RewindDTO: Codable {
-    var id: String
-    var timestamp: String
-    var _type: String = EventType.rewind.rawValue
-}
-
-struct PrimeDTO: Codable {
-    var id: String
-    var timestamp: String
-    var _type: String = EventType.prime.rawValue
-}
-
-// Mask distinct DTO subtypes with a common enum that conforms to Encodable
-enum PumpEventDTO: Encodable {
-    case bolus(BolusDTO)
-    case tempBasal(TempBasalDTO)
-    case tempBasalDuration(TempBasalDurationDTO)
-    case suspend(SuspendDTO)
-    case resume(ResumeDTO)
-    case rewind(RewindDTO)
-    case prime(PrimeDTO)
-
-    func encode(to encoder: Encoder) throws {
-        switch self {
-        case let .bolus(bolus):
-            try bolus.encode(to: encoder)
-        case let .tempBasal(tempBasal):
-            try tempBasal.encode(to: encoder)
-        case let .tempBasalDuration(tempBasalDuration):
-            try tempBasalDuration.encode(to: encoder)
-        case let .suspend(suspend):
-            try suspend.encode(to: encoder)
-        case let .resume(resume):
-            try resume.encode(to: encoder)
-        case let .rewind(rewind):
-            try rewind.encode(to: encoder)
-        case let .prime(prime):
-            try prime.encode(to: encoder)
-        }
-    }
-}
-
-// Extension with helper functions to map pump events to DTO objects via uniform masking enum
 extension PumpEventStored {
-    static let dateFormatter: ISO8601DateFormatter = {
-        let formatter = ISO8601DateFormatter()
-        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-        return formatter
-    }()
+    /// Converts a stored pump event into the `PumpHistoryEvent`s the oref algorithm can use.
+    /// A temp basal yields a duration entry followed by a rate entry.
+    func toPumpHistoryEvents() -> [PumpHistoryEvent] {
+        var events: [PumpHistoryEvent] = []
+        if let bolus = toBolusPumpHistoryEvent() { events.append(bolus) }
+        if let duration = toTempBasalDurationPumpHistoryEvent() { events.append(duration) }
+        if let tempBasal = toTempBasalPumpHistoryEvent() { events.append(tempBasal) }
+        if let suspend = toSuspendPumpHistoryEvent() { events.append(suspend) }
+        if let resume = toResumePumpHistoryEvent() { events.append(resume) }
+        if let rewind = toRewindPumpHistoryEvent() { events.append(rewind) }
+        if let prime = toPrimePumpHistoryEvent() { events.append(prime) }
+        return events
+    }
 
-    func toBolusDTOEnum() -> PumpEventDTO? {
+    private func toBolusPumpHistoryEvent() -> PumpHistoryEvent? {
         guard let timestamp = timestamp, let bolus = bolus, let amount = bolus.amount else {
             return nil
         }
-
-        let bolusDTO = BolusDTO(
+        return PumpHistoryEvent(
             id: id ?? UUID().uuidString,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
-            amount: amount.doubleValue,
-            isExternal: bolus.isExternal,
+            type: .bolus,
+            timestamp: timestamp,
+            amount: Decimal(algorithmValue: amount.doubleValue),
+            duration: 0,
             isSMB: bolus.isSMB,
-            duration: 0
+            isExternal: bolus.isExternal
         )
-        return .bolus(bolusDTO)
     }
 
-    func toTempBasalDTOEnum() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
+    // The temp basal duration populates `durationMin`, not `duration`.
+    private func toTempBasalDurationPumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
             return nil
         }
-
-        let tempBasalDTO = TempBasalDTO(
-            id: "_\(id)",
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
-            temp: tempBasal.tempType ?? "unknown",
-            rate: rate.doubleValue
+        return PumpHistoryEvent(
+            id: id,
+            type: .tempBasalDuration,
+            timestamp: timestamp,
+            durationMin: Int(tempBasal.duration)
         )
-        return .tempBasal(tempBasalDTO)
     }
 
-    func toTempBasalDurationDTOEnum() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
+    // The temp basal rate entry id is prefixed with "_". An unrecognized `tempType` maps to nil.
+    private func toTempBasalPumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
             return nil
         }
-
-        let tempBasalDurationDTO = TempBasalDurationDTO(
-            id: id,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
-            duration: Int(tempBasal.duration)
+        return PumpHistoryEvent(
+            id: "_\(id)",
+            type: .tempBasal,
+            timestamp: timestamp,
+            rate: Decimal(algorithmValue: rate.doubleValue),
+            temp: tempBasal.tempType.flatMap { Trio.TempType(rawValue: $0) }
         )
-        return .tempBasalDuration(tempBasalDurationDTO)
     }
 
-    func toPumpSuspendDTO() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpSuspend.rawValue else {
+    private func toSuspendPumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, type == EventType.pumpSuspend.rawValue else {
             return nil
         }
-
-        let suspendDTO = SuspendDTO(
-            id: id,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
-        )
-        return .suspend(suspendDTO)
+        return PumpHistoryEvent(id: id, type: .pumpSuspend, timestamp: timestamp)
     }
 
-    func toPumpResumeDTO() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpResume.rawValue else {
+    private func toResumePumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, type == EventType.pumpResume.rawValue else {
             return nil
         }
-
-        let resumeDTO = ResumeDTO(
-            id: id,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
-        )
-        return .resume(resumeDTO)
+        return PumpHistoryEvent(id: id, type: .pumpResume, timestamp: timestamp)
     }
 
-    func toRewindDTO() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.rewind.rawValue else {
+    private func toRewindPumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, type == EventType.rewind.rawValue else {
             return nil
         }
-
-        let rewindDTO = RewindDTO(
-            id: id,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
-        )
-        return .rewind(rewindDTO)
+        return PumpHistoryEvent(id: id, type: .rewind, timestamp: timestamp)
     }
 
-    func toPrimeDTO() -> PumpEventDTO? {
-        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.prime.rawValue else {
+    private func toPrimePumpHistoryEvent() -> PumpHistoryEvent? {
+        guard let id = id, let timestamp = timestamp, type == EventType.prime.rawValue else {
             return nil
         }
-
-        let primeDTO = PrimeDTO(
-            id: id,
-            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
-        )
-        return .prime(primeDTO)
+        return PumpHistoryEvent(id: id, type: .prime, timestamp: timestamp)
     }
 }

+ 12 - 24
Trio.xcodeproj/project.pbxproj

@@ -101,8 +101,6 @@
 		383420D625FFE38C002D46C1 /* LoopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D525FFE38C002D46C1 /* LoopView.swift */; };
 		383420D925FFEB3F002D46C1 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D825FFEB3F002D46C1 /* Popup.swift */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
-		384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803325C385E60086DB71 /* JavaScriptWorker.swift */; };
-		384E803825C388640086DB71 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803725C388640086DB71 /* Script.swift */; };
 		38569347270B5DFB0002C50D /* CGMType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569344270B5DFA0002C50D /* CGMType.swift */; };
 		38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569345270B5DFA0002C50D /* GlucoseSource.swift */; };
 		38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569346270B5DFB0002C50D /* AppGroupSource.swift */; };
@@ -118,7 +116,6 @@
 		38887CCE25F5725200944304 /* IOBEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38887CCD25F5725200944304 /* IOBEntry.swift */; };
 		388E595C25AD948C0019842D /* TrioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E595B25AD948C0019842D /* TrioApp.swift */; };
 		388E596C25AD95110019842D /* OpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E596B25AD95110019842D /* OpenAPS.swift */; };
-		388E596F25AD96040019842D /* javascript in Resources */ = {isa = PBXBuildFile; fileRef = 388E596E25AD96040019842D /* javascript */; };
 		388E597225AD9CF10019842D /* json in Resources */ = {isa = PBXBuildFile; fileRef = 388E597125AD9CF10019842D /* json */; };
 		388E5A5C25B6F0770019842D /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E5A5B25B6F0770019842D /* JSON.swift */; };
 		388E5A6025B6F2310019842D /* Autosens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E5A5F25B6F2310019842D /* Autosens.swift */; };
@@ -259,7 +256,6 @@
 		3B506FD92E635304000740B9 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B506FD82E635304000740B9 /* IOBService.swift */; };
 		3B56079E2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B56079D2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift */; };
 		3B5607A02ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B56079F2ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift */; };
-		3B5CD1EC2D4912A600CE213C /* OpenAPSSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD1E92D4912A600CE213C /* OpenAPSSwift.swift */; };
 		3B5CD1ED2D4912A600CE213C /* JSONBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD1EA2D4912A600CE213C /* JSONBridge.swift */; };
 		3B5CD2982D4AEA3C00CE213C /* Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2922D4AEA3C00CE213C /* Carbs.swift */; };
 		3B5CD2992D4AEA3C00CE213C /* Isf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2932D4AEA3C00CE213C /* Isf.swift */; };
@@ -303,7 +299,6 @@
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		3BE2F1E82E030E2F009E2900 /* MealCobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE2F1E72E030E2F009E2900 /* MealCobTests.swift */; };
 		3BE2F1EA2E031951009E2900 /* MealCobBucketingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE2F1E92E031951009E2900 /* MealCobBucketingTests.swift */; };
-		3BEA3AE02D58F79700A67A1D /* OrefFunctionResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEA3ADE2D58F79700A67A1D /* OrefFunctionResult.swift */; };
 		3BEDC9932EEB27B600AC6492 /* IobConsecutiveEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEDC9922EEB27B600AC6492 /* IobConsecutiveEventsTests.swift */; };
 		3BEF6AB12D9731660076089D /* MealHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB02D9731530076089D /* MealHistoryTests.swift */; };
 		3BEF6AB32D97316F0076089D /* MealTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB22D97316A0076089D /* MealTotalTests.swift */; };
@@ -800,7 +795,6 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
-		DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */; };
 		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E2842D2361F800C2988C /* LoopStatusView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
@@ -834,6 +828,9 @@
 		DDD7C8C12F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */; };
 		DDD7C8C22F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */; };
 		DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */; };
+		DDDD0FFD2F4E22C000F9C645 /* GlucoseNativeConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD0FFC2F4E22C000F9C645 /* GlucoseNativeConversionTests.swift */; };
+		DDDD10A12F4E22C000F9C645 /* CarbsNativeConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD10A02F4E22C000F9C645 /* CarbsNativeConversionTests.swift */; };
+		DDDD10B12F4E22C000F9C645 /* PumpHistoryNativeConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD10B02F4E22C000F9C645 /* PumpHistoryNativeConversionTests.swift */; };
 		DDDD0FFF2F4E231B00F9C645 /* MockTDDStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD0FFE2F4E231B00F9C645 /* MockTDDStorage.swift */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
@@ -1131,8 +1128,6 @@
 		383420D525FFE38C002D46C1 /* LoopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopView.swift; sourceTree = "<group>"; };
 		383420D825FFEB3F002D46C1 /* Popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Popup.swift; sourceTree = "<group>"; };
 		383948D525CD4D8900E91849 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = "<group>"; };
-		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
-		384E803725C388640086DB71 /* Script.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Script.swift; sourceTree = "<group>"; };
 		38569344270B5DFA0002C50D /* CGMType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMType.swift; sourceTree = "<group>"; };
 		38569345270B5DFA0002C50D /* GlucoseSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseSource.swift; sourceTree = "<group>"; };
 		38569346270B5DFB0002C50D /* AppGroupSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppGroupSource.swift; sourceTree = "<group>"; };
@@ -1150,7 +1145,6 @@
 		388E595B25AD948C0019842D /* TrioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioApp.swift; sourceTree = "<group>"; };
 		388E596425AD948E0019842D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		388E596B25AD95110019842D /* OpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPS.swift; sourceTree = "<group>"; };
-		388E596E25AD96040019842D /* javascript */ = {isa = PBXFileReference; lastKnownFileType = folder; path = javascript; sourceTree = "<group>"; };
 		388E597125AD9CF10019842D /* json */ = {isa = PBXFileReference; lastKnownFileType = folder; path = json; sourceTree = "<group>"; };
 		388E5A5B25B6F0770019842D /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
 		388E5A5F25B6F2310019842D /* Autosens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Autosens.swift; sourceTree = "<group>"; };
@@ -1274,7 +1268,6 @@
 		3B506FD82E635304000740B9 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
 		3B56079D2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeletedGlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		3B56079F2ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeletedGlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		3B5CD1E92D4912A600CE213C /* OpenAPSSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSSwift.swift; sourceTree = "<group>"; };
 		3B5CD1EA2D4912A600CE213C /* JSONBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONBridge.swift; sourceTree = "<group>"; };
 		3B5CD2912D4AEA3C00CE213C /* Basal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basal.swift; sourceTree = "<group>"; };
 		3B5CD2922D4AEA3C00CE213C /* Carbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carbs.swift; sourceTree = "<group>"; };
@@ -1317,7 +1310,6 @@
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BE2F1E72E030E2F009E2900 /* MealCobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealCobTests.swift; sourceTree = "<group>"; };
 		3BE2F1E92E031951009E2900 /* MealCobBucketingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealCobBucketingTests.swift; sourceTree = "<group>"; };
-		3BEA3ADE2D58F79700A67A1D /* OrefFunctionResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrefFunctionResult.swift; sourceTree = "<group>"; };
 		3BEDC9922EEB27B600AC6492 /* IobConsecutiveEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobConsecutiveEventsTests.swift; sourceTree = "<group>"; };
 		3BEF6AB02D9731530076089D /* MealHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealHistoryTests.swift; sourceTree = "<group>"; };
 		3BEF6AB22D97316A0076089D /* MealTotalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealTotalTests.swift; sourceTree = "<group>"; };
@@ -1813,7 +1805,6 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
-		DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmGlucose.swift; sourceTree = "<group>"; };
 		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDA6E2842D2361F800C2988C /* LoopStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
@@ -1850,6 +1841,9 @@
 		DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSmoothingTests.swift; sourceTree = "<group>"; };
+		DDDD0FFC2F4E22C000F9C645 /* GlucoseNativeConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNativeConversionTests.swift; sourceTree = "<group>"; };
+		DDDD10A02F4E22C000F9C645 /* CarbsNativeConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsNativeConversionTests.swift; sourceTree = "<group>"; };
+		DDDD10B02F4E22C000F9C645 /* PumpHistoryNativeConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryNativeConversionTests.swift; sourceTree = "<group>"; };
 		DDDD0FFE2F4E231B00F9C645 /* MockTDDStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTDDStorage.swift; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
@@ -2503,7 +2497,6 @@
 			isa = PBXGroup;
 			children = (
 				388E597125AD9CF10019842D /* json */,
-				388E596E25AD96040019842D /* javascript */,
 				BD1179332F4E22C100F90001 /* Sounds */,
 				3811DEC725C9DA7300A708ED /* Trio.entitlements */,
 				388E596425AD948E0019842D /* Info.plist */,
@@ -2730,8 +2723,6 @@
 			isa = PBXGroup;
 			children = (
 				388E596B25AD95110019842D /* OpenAPS.swift */,
-				384E803325C385E60086DB71 /* JavaScriptWorker.swift */,
-				384E803725C388640086DB71 /* Script.swift */,
 				3821ED4B25DD18BA00BC42AD /* Constants.swift */,
 			);
 			path = OpenAPS;
@@ -2742,7 +2733,6 @@
 			children = (
 				BD11795B2F4E22C100F90001 /* GlucoseAlerts */,
 				49C782A62F73D9870062B0DD /* AlertEntry.swift */,
-				DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */,
 				3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
@@ -2988,6 +2978,9 @@
 			children = (
 				DDDD0FFD2F4E231600F9C645 /* Mocks */,
 				DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */,
+				DDDD0FFC2F4E22C000F9C645 /* GlucoseNativeConversionTests.swift */,
+				DDDD10A02F4E22C000F9C645 /* CarbsNativeConversionTests.swift */,
+				DDDD10B02F4E22C000F9C645 /* PumpHistoryNativeConversionTests.swift */,
 				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
@@ -3082,8 +3075,6 @@
 				3B5CD2B22D4AEA6600CE213C /* Models */,
 				3B5CD2972D4AEA3C00CE213C /* Profile */,
 				3B5CD2A02D4AEA5100CE213C /* Utils */,
-				3B5CD1E92D4912A600CE213C /* OpenAPSSwift.swift */,
-				3BEA3ADE2D58F79700A67A1D /* OrefFunctionResult.swift */,
 				3B5CD1EA2D4912A600CE213C /* JSONBridge.swift */,
 			);
 			path = OpenAPSSwift;
@@ -4787,7 +4778,6 @@
 				38DF178E27733E6800B3528F /* Assets.xcassets in Resources */,
 				19DA48E829CD339B00EEA1E7 /* Assets.xcassets in Resources */,
 				8A91342A2D63D9A1007F8874 /* Localizable.xcstrings in Resources */,
-				388E596F25AD96040019842D /* javascript in Resources */,
 				BD1179342F4E22C100F90001 /* Sounds in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -4912,7 +4902,6 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */,
 				DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */,
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */,
@@ -4987,7 +4976,6 @@
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */,
 				DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */,
-				384E803825C388640086DB71 /* Script.swift in Sources */,
 				CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
 				38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */,
@@ -5118,7 +5106,6 @@
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
-				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
@@ -5150,7 +5137,6 @@
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
-				3BEA3AE02D58F79700A67A1D /* OrefFunctionResult.swift in Sources */,
 				BD47FD1B2D88AB4F0043966B /* OnboardingStateModel.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
@@ -5441,7 +5427,6 @@
 				C29835B02E2AA3F30068C5BB /* NightscoutUploadStepView.swift in Sources */,
 				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
-				3B5CD1EC2D4912A600CE213C /* OpenAPSSwift.swift in Sources */,
 				3B5CD1ED2D4912A600CE213C /* JSONBridge.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
@@ -5657,6 +5642,9 @@
 				3BBC22632DF5B94100169236 /* AutosensTests.swift in Sources */,
 				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */,
+				DDDD0FFD2F4E22C000F9C645 /* GlucoseNativeConversionTests.swift in Sources */,
+				DDDD10A12F4E22C000F9C645 /* CarbsNativeConversionTests.swift in Sources */,
+				DDDD10B12F4E22C000F9C645 /* PumpHistoryNativeConversionTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				CA02000000000000000010C2 /* DeliveryLimitsSyncTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,

File diff suppressed because it is too large
+ 0 - 2
Trio/Resources/javascript/bundle/autosens.js


File diff suppressed because it is too large
+ 0 - 1
Trio/Resources/javascript/bundle/autotune-core.js


File diff suppressed because it is too large
+ 0 - 2
Trio/Resources/javascript/bundle/autotune-prep.js


File diff suppressed because it is too large
+ 0 - 1
Trio/Resources/javascript/bundle/basal-set-temp.js


File diff suppressed because it is too large
+ 0 - 1
Trio/Resources/javascript/bundle/determine-basal.js


File diff suppressed because it is too large
+ 0 - 1
Trio/Resources/javascript/bundle/glucose-get-last.js


File diff suppressed because it is too large
+ 0 - 2
Trio/Resources/javascript/bundle/iob.js


File diff suppressed because it is too large
+ 0 - 2
Trio/Resources/javascript/bundle/meal.js


File diff suppressed because it is too large
+ 0 - 2
Trio/Resources/javascript/bundle/profile.js


+ 0 - 5
Trio/Resources/javascript/middleware/determine_basal.js

@@ -1,5 +0,0 @@
-function middleware(iob, currenttemp, glucose, profile, autosens, meal, reservoir, clock, pumphistory, preferences, basalprofile) {
-    // modify anything
-    // return any reason what has changed.
-    return "Nothing changed";
-}

+ 0 - 26
Trio/Resources/javascript/prepare/autosens.js

@@ -1,26 +0,0 @@
-// для settings/autosens.json параметры: monitor/glucose.json monitor/pumphistory-24h-zoned.json settings/basal_profile.json settings/profile.json monitor/carbhistory.json settings/temptargets.json
-
-function generate(glucose_data, pumphistory_data, basalprofile, profile_data, carb_data = {}, temptarget_data = {}) {
-    if (glucose_data.length < 72) {
-        return { "ratio": 1, "error": "not enough glucose data to calculate autosens" };
-    };
-    
-    var iob_inputs = {
-        history: pumphistory_data,
-        profile: profile_data
-    };
-
-    var detection_inputs = {
-        iob_inputs: iob_inputs,
-        carbs: carb_data,
-        glucose_data: glucose_data,
-        basalprofile: basalprofile,
-        temptargets: temptarget_data
-    };
-    detection_inputs.deviations = 96;
-    var ratio8h = trio_autosens(detection_inputs);
-    detection_inputs.deviations = 288;
-    var ratio24h = trio_autosens(detection_inputs);
-    var lowestRatio = ratio8h.ratio < ratio24h.ratio ? ratio8h : ratio24h;
-    return lowestRatio;
-}

+ 0 - 17
Trio/Resources/javascript/prepare/autotune-core.js

@@ -1,17 +0,0 @@
-function generate(prepped_glucose_data,previous_autotune_data,pumpprofile_data) {
-  if (!pumpprofile_data.useCustomPeakTime) {
-      previous_autotune_data.dia = pumpprofile_data.dia;
-      previous_autotune_data.insulinPeakTime = pumpprofile_data.insulinPeakTime;
-    };
-
-    // Always keep the curve value up to date with what's in the user preferences
-    previous_autotune_data.curve = pumpprofile_data.curve;
-
-    inputs = {
-        preppedGlucose: prepped_glucose_data
-      , previousAutotune: previous_autotune_data
-      , pumpProfile: pumpprofile_data
-    };
-
-    return trio_autotuneCore(inputs);
-}

+ 0 - 47
Trio/Resources/javascript/prepare/autotune-prep.js

@@ -1,47 +0,0 @@
-function generate(pumphistory_data, profile_data, glucose_data, pumpprofile_data, carb_data = {} , categorize_uam_as_basal = false, tune_insulin_curve = false) {
-    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 0.1) {
-        if (typeof(pumpprofile_data.carb_ratio) === 'undefined' || pumpprofile_data.carb_ratio < 0.1) {
-            console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ' + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + ' out of bounds" }');
-            return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds");
-        } else {
-            profile_data.carb_ratio = pumpprofile_data.carb_ratio;
-        }
-    }
-
-    // get insulin curve from pump profile that is maintained
-    profile_data.curve = pumpprofile_data.curve;
-
-    // Pump profile has an up to date copy of useCustomPeakTime from preferences
-    // If the preferences file has useCustomPeakTime use the previous autotune dia and PeakTime.
-    // Otherwise, use data from pump profile.
-    if (!pumpprofile_data.useCustomPeakTime) {
-      profile_data.dia = pumpprofile_data.dia;
-      profile_data.insulinPeakTime = pumpprofile_data.insulinPeakTime;
-    }
-
-    // Always keep the curve value up to date with what's in the user preferences
-    profile_data.curve = pumpprofile_data.curve;
-
-    // Have to sort history - NS sort doesn't account for different zulu and local timestamps
-    pumphistory_data.sort( function( firstValue, secondValue ) {
-        try {
-            var a = new Date(firstValue.timestamp);
-            var b = new Date(secondValue.timestamp);
-            return b.getTime() - a.getTime();
-        } catch(e) {
-            return 0;
-        }
-    } );
-
-    inputs = {
-      history: pumphistory_data
-    , profile: profile_data
-    , pumpprofile: pumpprofile_data
-    , carbs: carb_data
-    , glucose: glucose_data
-    , categorize_uam_as_basal: categorize_uam_as_basal
-    , tune_insulin_curve: tune_insulin_curve
-    };
-
-    return trio_autotunePrep(inputs);
-}

+ 0 - 49
Trio/Resources/javascript/prepare/determine-basal.js

@@ -1,49 +0,0 @@
-//для enact/smb-suggested.json параметры: monitor/iob.json monitor/temp_basal.json monitor/glucose.json settings/profile.json settings/autosens.json --meal monitor/meal.json --microbolus --reservoir monitor/reservoir.json
-
-function generate(iob, currenttemp, glucose, profile, autosens = null, meal = null, microbolusAllowed = false, reservoir = null, clock = new Date(), pump_history, preferences, basalProfile, trio_custom_oref_variables) {
-
-    var clock = new Date();
-    
-    var middleware_was_used = "";
-    try {
-        var middlewareReason = middleware(iob, currenttemp, glucose, profile, autosens, meal, reservoir, clock, pump_history, preferences, basalProfile, trio_custom_oref_variables);
-        middleware_was_used = (middlewareReason || "Nothing changed");
-        console.log("Middleware reason: " + middleware_was_used);
-    } catch (error) {
-        console.log("Invalid middleware: " + error);
-    };
-
-    var glucose_status = trio_glucoseGetLast(glucose);
-    var autosens_data = null;
-
-    if (autosens) {
-        autosens_data = autosens;
-    }
-    
-    var reservoir_data = null;
-    if (reservoir) {
-        reservoir_data = reservoir;
-    }
-
-    var meal_data = {};
-    if (meal) {
-        meal_data = meal;
-    }
-    
-    var pumphistory = {};
-    if (pump_history) {
-        pumphistory = pump_history;
-    }
-    
-    var basalprofile = {};
-    if (basalProfile) {
-        basalprofile = basalProfile;
-    }
-    
-    var trio_custom_oref_variables_temp = {};
-    if (trio_custom_oref_variables) {
-        trio_custom_oref_variables_temp = trio_custom_oref_variables;
-    }
-    
-    return trio_determineBasal(glucose_status, currenttemp, iob, profile, autosens_data, meal_data, trio_basalSetTemp, microbolusAllowed, reservoir_data, clock, pumphistory, preferences, basalprofile, trio_custom_oref_variables_temp, middleware_was_used);
-}

+ 0 - 15
Trio/Resources/javascript/prepare/iob.js

@@ -1,15 +0,0 @@
-//для monitor/iob.json параметры: monitor/pumphistory-24h-zoned.json settings/profile.json monitor/clock-zoned.json settings/autosens.json
-
-function generate(pumphistory_data, profile_data, clock_data, autosens_data = null) {
-    var inputs = {
-        history: pumphistory_data
-        , history24: null
-        , profile: profile_data
-        , clock: clock_data
-    };
-
-      if (autosens_data) {
-        inputs.autosens = autosens_data;
-      }
-      return trio_iob(inputs);
-}

+ 0 - 6
Trio/Resources/javascript/prepare/log.js

@@ -1,6 +0,0 @@
-var console = {
-    log: function(...args) { _consoleLog(args); },
-    error: function(...args) { _consoleLog(args); }
-};
-var printLog = function(...args) { console.log(args); };
-var process = { stderr: { write: printLog } };

+ 0 - 35
Trio/Resources/javascript/prepare/meal.js

@@ -1,35 +0,0 @@
-//для monitor/meal.json параметры: monitor/pumphistory-24h-zoned.json settings/profile.json monitor/clock-zoned.json monitor/glucose.json settings/basal_profile.json monitor/carbhistory.json
-
-function generate(pumphistory_data, profile_data, clock_data, glucose_data, basalprofile_data, carbhistory = false) {
-    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 0.1) {
-        return {"error":"Error: carb_ratio " + profile_data.carb_ratio + " out of bounds"};
-    }
-
-    var carb_data = { };
-    if (carbhistory) {
-        carb_data = carbhistory;
-    }
-
-    if (typeof basalprofile_data[0] === 'undefined') {
-        return { "error":"Error: bad basalprofile_data: " + JSON.stringify(basalprofile_data) };
-    }
-
-    var inputs = {
-      history: pumphistory_data
-    , profile: profile_data
-    , basalprofile: basalprofile_data
-    , clock: clock_data
-    , carbs: carb_data
-    , glucose: glucose_data
-    };
-
-    var recentCarbs = trio_meal(inputs);
-
-    if (glucose_data.length < 4) {
-        console.error("Not enough glucose data to calculate carb absorption; found:", glucose_data.length);
-        recentCarbs.mealCOB = 0;
-        recentCarbs.reason = "not enough glucose data to calculate carb absorption";
-    }
-
-    return recentCarbs;
-}

+ 0 - 107
Trio/Resources/javascript/prepare/profile.js

@@ -1,107 +0,0 @@
-//для pumpprofile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json
-//для profile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json settings/autotune.json
-
-function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data, preferences_input = false, carbratio_input = false, temptargets_input = false, model_input = false, autotune_input = false, trio_data) {
-    if (bgtargets_data.units !== 'mg/dL') {
-        if (bgtargets_data.units === 'mmol/L') {
-            for (var i = 0, len = bgtargets_data.targets.length; i < len; i++) {
-                bgtargets_data.targets[i].high = bgtargets_data.targets[i].high * 18;
-                bgtargets_data.targets[i].low = bgtargets_data.targets[i].low * 18;
-            }
-            bgtargets_data.units = 'mg/dL';
-        } else {
-            return { "error" : 'BG Target data is expected to be expressed in mg/dL or mmol/L. Found '+ bgtargets_data.units };
-        }
-    }
-    
-    if (isf_data.units !== 'mg/dL') {
-        if (isf_data.units === 'mmol/L') {
-            for (var i = 0, len = isf_data.sensitivities.length; i < len; i++) {
-                isf_data.sensitivities[i].sensitivity = isf_data.sensitivities[i].sensitivity * 18;
-            }
-            isf_data.units = 'mg/dL';
-        } else {
-            return { "error" : 'ISF is expected to be expressed in mg/dL or mmol/L. Found '+ isf_data.units };
-        }
-    }
-
-    var autotune_data = { };
-    if (autotune_input) {
-        autotune_data = autotune_input;
-    }
-
-    var temptargets_data = { };
-    if (temptargets_input) {
-        temptargets_data = temptargets_input;
-    }
-    
-    var trioData = { };
-    if (trio_data) {
-        trioData = trio_data;
-    }
-
-    var model_data = { };
-    if (model_input) {
-        model_data = model_input.replace(/"/gi, '');
-    }
-
-    var carbratio_data = { };
-    if (carbratio_input) {
-        var errors = [ ];
-        if (!(carbratio_input.schedule && carbratio_input.schedule[0].start && carbratio_input.schedule[0].ratio)) {
-          errors.push("Carb ratio data should have an array called schedule with a start and ratio fields.");
-        }
-        if (carbratio_input.units !== 'grams' && carbratio_input.units !== 'exchanges')  {
-          errors.push("Carb ratio should have units field set to 'grams' or 'exchanges'.");
-        }
-        if (errors.length) {
-          return { "error" : errors.join(' ') };
-        }
-        carbratio_data = carbratio_input;
-    }
-
-    var preferences = { };
-    if (preferences_input) {
-        preferences = preferences_input;
-        if (preferences.curve === "rapid-acting") {
-            if (preferences.useCustomPeakTime) {
-                preferences.insulinPeakTime =
-                Math.max(50, Math.min(preferences.insulinPeakTime, 120));
-            } else { preferences.insulinPeakTime = 75; }
-        } 
-        else if (preferences.curve === "ultra-rapid") {
-            if (preferences.useCustomPeakTime) {
-                preferences.insulinPeakTime =
-                Math.max(35, Math.min(preferences.insulinPeakTime, 100));
-            } else { preferences.insulinPeakTime = 55; }
-        }
-    }
-
-    var inputs = { };
-    //add all preferences to the inputs
-    for (var pref in preferences) {
-      if (preferences.hasOwnProperty(pref)) {
-        inputs[pref] = preferences[pref];
-      }
-    }
-
-    inputs.max_iob = inputs.max_iob || 0;
-    //set these after to make sure nothing happens if they are also set in preferences
-    inputs.settings = pumpsettings_data;
-    inputs.targets = bgtargets_data;
-    inputs.basals = basalprofile_data;
-    inputs.isf = isf_data;
-    inputs.carbratio = carbratio_data;
-    inputs.temptargets = temptargets_data;
-    inputs.model = model_data;
-    inputs.autotune = autotune_data;
-
-    if (autotune_data) {
-        if (autotune_data.basalprofile) { inputs.basals = autotune_data.basalprofile; }
-        if (!trioData.onlyAutotuneBasals) {
-            if (autotune_data.isfProfile) { inputs.isf = autotune_data.isfProfile; }
-            if (autotune_data.carb_ratio) { inputs.carbratio.schedule[0].ratio = autotune_data.carb_ratio; }
-        }
-    }
-    return trio_profile(inputs);
-}

+ 5 - 7
Trio/Sources/APS/APSManager.swift

@@ -100,6 +100,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var settingsManager: SettingsManager!
@@ -167,7 +168,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     init(resolver: Resolver) {
         injectServices(resolver)
-        openAPS = OpenAPS(storage: storage, tddStorage: tddStorage)
+        openAPS = OpenAPS(storage: storage, tddStorage: tddStorage, glucoseStorage: glucoseStorage, carbsStorage: carbsStorage)
         subscribe()
         lastLoopDateSubject.send(lastLoopDate)
 
@@ -182,7 +183,7 @@ final class BaseAPSManager: APSManager, Injectable {
             if wasParsed {
                 Task {
                     do {
-                        try await openAPS.createProfiles(useJavascriptOref: settings.useJavascriptOref)
+                        try await openAPS.createProfiles()
                     } catch {
                         debug(
                             .apsManager,
@@ -435,8 +436,7 @@ final class BaseAPSManager: APSManager, Injectable {
               (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
         else {
             let result = try await openAPS.autosense(
-                shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
-                useJavascriptOref: settings.useJavascriptOref
+                shouldSmoothGlucose: settingsManager.settings.smoothGlucose
             )
             return result != nil
         }
@@ -515,14 +515,13 @@ final class BaseAPSManager: APSManager, Injectable {
             let now = Date()
 
             // put profile creation up front since autosens needs it
-            try await openAPS.createProfiles(useJavascriptOref: settings.useJavascriptOref)
+            try await openAPS.createProfiles()
             let currentTemp = try await fetchCurrentTempBasal(date: now)
             _ = try await autosense()
 
             let determination = try await openAPS.determineBasal(
                 currentTemp: currentTemp,
                 shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
-                useJavascriptOref: settings.useJavascriptOref,
                 clock: now
             )
             iobFileDidUpdate.send(())
@@ -569,7 +568,6 @@ final class BaseAPSManager: APSManager, Injectable {
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
-                useJavascriptOref: settings.useJavascriptOref,
                 clock: Date(),
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,

+ 5 - 0
Trio/Sources/APS/Extensions/DecimalExtensions.swift

@@ -4,6 +4,11 @@ extension Decimal {
     func clamp(to pickerSetting: PickerSetting) -> Decimal {
         max(min(self, pickerSetting.max), pickerSetting.min)
     }
+
+    /// Converts a `Double` to a `Decimal` using JSON style conversion
+    init(algorithmValue value: Double) {
+        self = Decimal(string: value.description) ?? Decimal(value)
+    }
 }
 
 extension Collection where Element == Decimal {

+ 0 - 27
Trio/Sources/APS/OpenAPS/Constants.swift

@@ -1,31 +1,4 @@
 extension OpenAPS {
-    enum Bundle {
-        static let iob = "bundle/iob.js"
-        static let meal = "bundle/meal.js"
-        static let autotunePrep = "bundle/autotune-prep.js"
-        static let autotuneCore = "bundle/autotune-core.js"
-        static let getLastGlucose = "bundle/glucose-get-last.js"
-        static let basalSetTemp = "bundle/basal-set-temp.js"
-        static let determineBasal = "bundle/determine-basal.js"
-        static let autosens = "bundle/autosens.js"
-        static let profile = "bundle/profile.js"
-    }
-
-    enum Prepare {
-        static let iob = "prepare/iob.js"
-        static let meal = "prepare/meal.js"
-        static let autotunePrep = "prepare/autotune-prep.js"
-        static let autotuneCore = "prepare/autotune-core.js"
-        static let determineBasal = "prepare/determine-basal.js"
-        static let autosens = "prepare/autosens.js"
-        static let profile = "prepare/profile.js"
-        static let log = "prepare/log.js"
-    }
-
-    enum Middleware {
-        static let determineBasal = "middleware/determine_basal.js"
-    }
-
     enum Settings {
         static let preferences = "preferences.json"
         static let autotune = "settings/autotune.json"

+ 0 - 146
Trio/Sources/APS/OpenAPS/JavaScriptWorker.swift

@@ -1,146 +0,0 @@
-import Foundation
-import JavaScriptCore
-
-private let contextLock = NSRecursiveLock()
-
-extension String {
-    var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
-    var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
-    var camelCased: String {
-        guard !isEmpty else { return "" }
-        let parts = components(separatedBy: .alphanumerics.inverted)
-        let first = parts.first!.lowercasingFirst
-        let rest = parts.dropFirst().map(\.uppercasingFirst)
-        return ([first] + rest).joined()
-    }
-
-    var pascalCased: String {
-        guard !isEmpty else { return "" }
-        let parts = components(separatedBy: .alphanumerics.inverted)
-        let first = parts.first!.uppercasingFirst
-        let rest = parts.dropFirst().map(\.uppercasingFirst)
-        return ([first] + rest).joined()
-    }
-}
-
-final class JavaScriptWorker {
-    private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
-    private let virtualMachine: JSVirtualMachine
-    private var contextPool: [JSContext] = []
-    private let contextPoolLock = NSLock()
-
-    init(poolSize: Int = 5) {
-        virtualMachine = JSVirtualMachine()!
-        // Pre-create a pool of JSContext instances
-        for _ in 0 ..< poolSize {
-            contextPool.append(createContext())
-        }
-    }
-
-    private func createContext() -> JSContext {
-        let context = JSContext(virtualMachine: virtualMachine)!
-        context.exceptionHandler = { _, exception in
-            if let error = exception?.toString() {
-                warning(.openAPS, "JavaScript Error: \(error)")
-            }
-        }
-        let consoleLog: @convention(block) (String) -> Void = { [weak context] message in
-            guard let context = context else { return }
-            let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
-            if !trimmedMessage.isEmpty {
-                let fileName = context.objectForKeyedSubscript("scriptName").toString() ?? "Unknown"
-                let threadSafeLog = "\(trimmedMessage)"
-                self.processQueue.async(flags: .barrier) {
-                    self.outputLogs(for: fileName, message: threadSafeLog)
-                }
-            }
-        }
-        context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
-        return context
-    }
-
-    private func getContext() -> JSContext {
-        contextPoolLock.lock()
-        let context = contextPool.popLast() ?? createContext()
-        contextPoolLock.unlock()
-        return context
-    }
-
-    private func returnContext(_ context: JSContext) {
-        contextPoolLock.lock()
-        contextPool.append(context)
-        contextPoolLock.unlock()
-    }
-
-    private func outputLogs(for fileName: String, message: String) {
-        let logs = message.trimmingCharacters(in: .whitespacesAndNewlines)
-
-        if logs.isEmpty { return }
-
-        if fileName == "autosens.js" {
-            let sanitizedLogs = logs.split(separator: "\n").map { logLine in
-                logLine.replacingOccurrences(
-                    of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
-                    with: "",
-                    options: .regularExpression
-                )
-            }.joined(separator: "\n")
-
-            sanitizedLogs.split(separator: "\n").forEach { logLine in
-                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
-                    debug(.openAPS, "\(fileName): \(logLine)")
-                }
-            }
-        } else {
-            logs.split(separator: "\n").forEach { logLine in
-                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
-                    debug(.openAPS, "\(fileName): \(logLine)")
-                }
-            }
-        }
-    }
-
-    @discardableResult func evaluate(script: Script) -> JSValue! {
-        let context = getContext()
-        defer { returnContext(context) }
-        let fileName = URL(fileURLWithPath: script.name).lastPathComponent
-        context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
-        let result = context.evaluateScript(script.body)
-        return result
-    }
-
-    private func evaluate(string: String) -> JSValue! {
-        let context = getContext()
-        defer { returnContext(context) }
-        return context.evaluateScript(string)
-    }
-
-    private func json(for string: String) -> RawJSON {
-        evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
-    }
-
-    func call(function: String, with arguments: [JSON]) -> RawJSON {
-        let joined = arguments.map(\.rawJSON).joined(separator: ",")
-        return json(for: "\(function)(\(joined))")
-    }
-
-    func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
-        let context = getContext()
-        defer {
-            returnContext(context)
-        }
-        return execute(self)
-    }
-
-    func evaluateBatch(scripts: [Script]) {
-        let context = getContext()
-        defer {
-            returnContext(context)
-        }
-        scripts.forEach { script in
-            let fileName = URL(fileURLWithPath: script.name).lastPathComponent
-            context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
-            context.evaluateScript(script.body)
-        }
-    }
-}

File diff suppressed because it is too large
+ 201 - 640
Trio/Sources/APS/OpenAPS/OpenAPS.swift


+ 0 - 26
Trio/Sources/APS/OpenAPS/Script.swift

@@ -1,26 +0,0 @@
-import Foundation
-
-struct Script {
-    let name: String
-    let body: String
-
-    init(name: String) {
-        self.name = name
-        if let url = Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
-            do {
-                body = try String(contentsOf: url)
-            } catch {
-                debug(.openAPS, "Error loading script: \(error)")
-                body = "Error loading script"
-            }
-        } else {
-            print("Resource not found: javascript/\(name)")
-            body = "Resource not found"
-        }
-    }
-
-    init(name: String, body: String) {
-        self.name = name
-        self.body = body
-    }
-}

+ 0 - 70
Trio/Sources/APS/OpenAPSSwift/JSONBridge.swift

@@ -8,10 +8,6 @@ enum JSONError: Error {
 }
 
 enum JSONBridge {
-    static func preferences(from: JSON) throws -> Preferences {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
     static func pumpSettings(from: JSON) throws -> PumpSettings {
         try JSONBridge.from(string: from.rawJSON)
     }
@@ -40,84 +36,18 @@ enum JSONBridge {
         try JSONBridge.from(string: from.rawJSON)
     }
 
-    static func model(from: JSON) -> String {
-        from.rawJSON
-    }
-
-    static func trioSettings(from: JSON) throws -> TrioSettings {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
-    static func glucose(from: JSON) throws -> [BloodGlucose] {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
-    static func currentTemp(from: JSON) throws -> TempBasal {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
-    static func carbs(from: JSON) throws -> [CarbsEntry] {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
-    static func iobResult(from: JSON) throws -> [IobResult] {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
-    static func pumpHistory(from: JSON) throws -> [PumpHistoryEvent] {
-        do {
-            return try JSONBridge.from(string: from.rawJSON)
-        } catch {
-            // see if we got an empty object "{}"
-            guard let data = from.rawJSON.data(using: .utf8) else {
-                throw error
-            }
-
-            if let parsedObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
-               parsedObject.isEmpty
-            {
-                return []
-            }
-
-            throw error
-        }
-    }
-
     static func profile(from: JSON) throws -> Profile {
         try JSONBridge.from(string: from.rawJSON)
     }
 
-    static func computedCarbs(from: JSON) throws -> ComputedCarbs? {
-        try JSONBridge.from(string: from.rawJSON)
-    }
-
     static func autosens(from: JSON) throws -> Autosens? {
         try JSONBridge.from(string: from.rawJSON)
     }
 
-    static func clock(from: JSON) throws -> Date {
-        let dateJson = from.rawJSON.replacingOccurrences(of: "\"", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
-        if let date = Formatter.iso8601withFractionalSeconds.date(from: dateJson) ?? Formatter.iso8601
-            .date(from: dateJson)
-        {
-            return date
-        }
-
-        throw JSONError.invalidDate(from.rawJSON)
-    }
-
     static func from<T: Decodable>(string: String) throws -> T {
         guard let data = string.data(using: .utf8) else {
             throw JSONError.invalidString
         }
         return try JSONCoding.decoder.decode(T.self, from: data)
     }
-
-    static func to<T: Encodable>(_ value: T) throws -> String {
-        let data = try JSONCoding.encoder.encode(value)
-        guard let string = String(data: data, encoding: .utf8) else {
-            throw JSONError.encodingFailed
-        }
-        return string
-    }
 }

+ 1 - 1
Trio/Sources/APS/OpenAPSSwift/Models/IobResult.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct IobResult: Codable {
+struct IobResult: JSON {
     static func from(iob: IobTotal, iobWithZeroTemp: IobTotal) -> IobResult {
         IobResult(
             iob: iob.iob,

+ 1 - 1
Trio/Sources/APS/OpenAPSSwift/Models/Profile.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct Profile: Codable {
+struct Profile: JSON {
     // Kotlin-defined properties from AndroidAPS OapsProfile.kt
     // with defaults pulled from profile.js
     var dia: Decimal?

+ 0 - 210
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -1,210 +0,0 @@
-import Foundation
-
-struct OpenAPSSwift {
-    static func makeProfile(
-        preferences: JSON,
-        pumpSettings: JSON,
-        bgTargets: JSON,
-        basalProfile: JSON,
-        isf: JSON,
-        carbRatio: JSON,
-        tempTargets: JSON,
-        model: JSON,
-        trioSettings: JSON,
-        clock: Date
-    ) -> (OrefFunctionResult) {
-        do {
-            let preferences = try JSONBridge.preferences(from: preferences)
-            let pumpSettings = try JSONBridge.pumpSettings(from: pumpSettings)
-            let bgTargets = try JSONBridge.bgTargets(from: bgTargets)
-            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
-            let isf = try JSONBridge.insulinSensitivities(from: isf)
-            let carbRatio = try JSONBridge.carbRatios(from: carbRatio)
-            let tempTargets = try JSONBridge.tempTargets(from: tempTargets)
-            let model = JSONBridge.model(from: model)
-            let trioSettings = try JSONBridge.trioSettings(from: trioSettings)
-
-            let profile = try ProfileGenerator.generate(
-                pumpSettings: pumpSettings,
-                bgTargets: bgTargets,
-                basalProfile: basalProfile,
-                isf: isf,
-                preferences: preferences,
-                carbRatios: carbRatio,
-                tempTargets: tempTargets,
-                model: model,
-                clock: clock
-            )
-
-            return (try .success(JSONBridge.to(profile)))
-        } catch {
-            return (.failure(error))
-        }
-    }
-
-    static func determineBasal(
-        glucose: JSON,
-        currentTemp: JSON,
-        iob: JSON,
-        profile: JSON,
-        autosens: JSON,
-        meal: JSON,
-        microBolusAllowed: Bool,
-        reservoir: JSON,
-        pumpHistory: JSON,
-        preferences: JSON,
-        basalProfile: JSON,
-        trioCustomOrefVariables: JSON,
-        clock: Date
-    ) -> (OrefFunctionResult) {
-        do {
-            let glucose = try JSONBridge.glucose(from: glucose)
-            let currentTemp = try JSONBridge.currentTemp(from: currentTemp)
-            let iob = try JSONBridge.iobResult(from: iob)
-            let profile = try JSONBridge.profile(from: profile)
-            let autosens = try JSONBridge.autosens(from: autosens)
-            let meal = try JSONBridge.computedCarbs(from: meal)
-            let microBolusAllowed = microBolusAllowed
-            let reservoir = Decimal(string: reservoir.rawJSON)
-            let pumpHistory = try JSONBridge.pumpHistory(from: pumpHistory)
-            let preferences = try JSONBridge.preferences(from: preferences)
-            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
-            let trioCustomOrefVariables = try JSONBridge.trioCustomOrefVariables(from: trioCustomOrefVariables)
-
-            guard let mealData = meal, let autosensData = autosens else {
-                return .failure(DeterminationError.missingInputs)
-            }
-
-            let rawDetermination = try DeterminationGenerator.generate(
-                profile: profile,
-                preferences: preferences,
-                currentTemp: currentTemp,
-                iobData: iob,
-                mealData: mealData,
-                autosensData: autosensData,
-                reservoirData: reservoir ?? 100,
-                glucose: glucose,
-                microBolusAllowed: microBolusAllowed,
-                trioCustomOrefVariables: trioCustomOrefVariables,
-                currentTime: clock
-            )
-
-            return try .success(JSONBridge.to(rawDetermination))
-
-        } catch let determinationError as DeterminationError {
-            // if we get a determination error we want to return it as a JSON
-            // object that is { "error": "some error" }
-            do {
-                let response = try JSONBridge.to(DeterminationErrorResponse(error: determinationError.localizedDescription))
-                return .success(response)
-            } catch {
-                return .failure(determinationError)
-            }
-        } catch {
-            return .failure(error)
-        }
-    }
-
-    static func meal(
-        pumphistory: JSON,
-        profile: JSON,
-        basalProfile: JSON,
-        clock: JSON,
-        carbs: JSON,
-        glucose: JSON
-    ) -> (OrefFunctionResult) {
-        do {
-            let pumpHistory = try JSONBridge.pumpHistory(from: pumphistory)
-            let profile = try JSONBridge.profile(from: profile)
-            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
-            let clock = try JSONBridge.clock(from: clock)
-            let carbs = try JSONBridge.carbs(from: carbs)
-            let glucose = try JSONBridge.glucose(from: glucose)
-
-            let mealResult = try MealGenerator.generate(
-                pumpHistory: pumpHistory,
-                profile: profile,
-                basalProfile: basalProfile,
-                clock: clock,
-                carbHistory: carbs,
-                glucoseHistory: glucose
-            )
-
-            return try .success(JSONBridge.to(mealResult))
-        } catch {
-            return .failure(error)
-        }
-    }
-
-    static func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) -> (OrefFunctionResult) {
-        do {
-            let pumpHistory = try JSONBridge.pumpHistory(from: pumphistory)
-            let profile = try JSONBridge.profile(from: profile)
-            let clock = try JSONBridge.clock(from: clock)
-            let autosens = try JSONBridge.autosens(from: autosens)
-
-            let iobResult = try IobGenerator.generate(
-                history: pumpHistory,
-                profile: profile,
-                clock: clock,
-                autosens: autosens
-            )
-
-            return try .success(JSONBridge.to(iobResult))
-        } catch {
-            return .failure(error)
-        }
-    }
-
-    static func autosense(
-        glucose: JSON,
-        pumpHistory: JSON,
-        basalProfile: JSON,
-        profile: JSON,
-        carbs: JSON,
-        tempTargets: JSON,
-        clock: JSON,
-        includeDeviationsForTesting: Bool = false
-    ) -> (OrefFunctionResult) {
-        do {
-            let glucose = try JSONBridge.glucose(from: glucose)
-            let pumpHistory = try JSONBridge.pumpHistory(from: pumpHistory)
-            let basalProfile = try JSONBridge.basalProfile(from: basalProfile)
-            let profile = try JSONBridge.profile(from: profile)
-            let carbs = try JSONBridge.carbs(from: carbs)
-            let tempTargets = try JSONBridge.tempTargets(from: tempTargets)
-            let clock = try JSONBridge.clock(from: clock)
-
-            // this logic is from prepare/autosens.js
-            let ratio8h = try AutosensGenerator.generate(
-                glucose: glucose,
-                pumpHistory: pumpHistory,
-                basalProfile: basalProfile,
-                profile: profile,
-                carbs: carbs,
-                tempTargets: tempTargets,
-                maxDeviations: 96,
-                clock: clock,
-                includeDeviationsForTesting: includeDeviationsForTesting
-            )
-
-            let ratio24h = try AutosensGenerator.generate(
-                glucose: glucose,
-                pumpHistory: pumpHistory,
-                basalProfile: basalProfile,
-                profile: profile,
-                carbs: carbs,
-                tempTargets: tempTargets,
-                maxDeviations: 288,
-                clock: clock,
-                includeDeviationsForTesting: includeDeviationsForTesting
-            )
-
-            let lowestRatio = ratio8h.ratio < ratio24h.ratio ? ratio8h : ratio24h
-
-            return try .success(JSONBridge.to(lowestRatio))
-        } catch {
-            return .failure(error)
-        }
-    }
-}

+ 0 - 13
Trio/Sources/APS/OpenAPSSwift/OrefFunctionResult.swift

@@ -1,13 +0,0 @@
-import Foundation
-
-enum OrefFunctionResult {
-    case success(RawJSON)
-    case failure(Error)
-
-    func returnOrThrow() throws -> RawJSON {
-        switch self {
-        case let .success(json): return json
-        case let .failure(error): throw error
-        }
-    }
-}

+ 63 - 0
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -13,6 +13,7 @@ protocol CarbsStorage {
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async throws
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
+    func getCarbsForAlgorithm(additionalCarbs: Decimal?, carbsDate: Date?) async throws -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry]
@@ -350,6 +351,68 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         Date().addingTimeInterval(-1.days.timeInterval)
     }
 
+    /// Fetches the last day of carbs and converts them into the `CarbsEntry` values the oref algorithm
+    /// consumes, optionally appending a synthetic "additional carbs" entry for bolus simulation.
+    func getCarbsForAlgorithm(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> [CarbsEntry] {
+        let context = makeContext()
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false
+        )
+
+        return try await context.perform {
+            guard let carbResults = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+
+            var entries = carbResults.map { Self.mapToCarbsEntry($0) }
+
+            if let additionalCarbs = additionalCarbs {
+                entries.append(Self.additionalCarbsEntry(carbs: additionalCarbs, date: carbsDate ?? Date()))
+            }
+
+            return entries
+        }
+    }
+
+    /// Converts CoreData stored carb entries into a struct that the oref algorithm can use
+    static func mapToCarbsEntry(_ carbEntry: CarbEntryStored) -> CarbsEntry {
+        // The old encode used `date ?? Date()` for both created_at and actualDate.
+        let date = carbEntry.date ?? Date()
+        return CarbsEntry(
+            id: carbEntry.id?.uuidString,
+            createdAt: date,
+            actualDate: date,
+            carbs: Decimal(algorithmValue: carbEntry.carbs),
+            fat: Decimal(algorithmValue: carbEntry.fat),
+            protein: Decimal(algorithmValue: carbEntry.protein),
+            note: nil,
+            enteredBy: CarbsEntry.local,
+            isFPU: carbEntry.isFPU,
+            fpuID: nil
+        )
+    }
+
+    /// Builds the synthetic "additional carbs" entry that the determine-basal flow appends for bolus
+    /// simulation.
+    static func additionalCarbsEntry(carbs: Decimal, date: Date, id: String = UUID().uuidString) -> CarbsEntry {
+        CarbsEntry(
+            id: id,
+            createdAt: date,
+            actualDate: date,
+            carbs: carbs,
+            fat: 0,
+            protein: 0,
+            note: nil,
+            enteredBy: CarbsEntry.local,
+            isFPU: false,
+            fpuID: nil
+        )
+    }
+
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async {
         let context = makeContext()
         context.name = "deleteCarbsEntryStored"

+ 96 - 0
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -17,6 +17,7 @@ protocol GlucoseStorage {
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date?
     func isGlucoseFresh() -> Bool
+    func getGlucoseForAlgorithm(shouldSmoothGlucose: Bool, fetchHours: Decimal) async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
@@ -443,6 +444,101 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         ) as? [GlucoseStored] ?? []).first
     }
 
+    /// Fetches recent glucose and converts it into the `BloodGlucose` values the oref algorithm consumes.
+    ///
+    /// The fetch window is bounded by `fetchHours`: determineBasal feeds `maxMealAbsorptionTime + 0.5h`
+    /// (just enough glucose to cover the longest tracked meal absorption plus a small lead-in); Autosens
+    /// uses the default 24h because its sensitivity algorithm needs that full window.
+    func getGlucoseForAlgorithm(shouldSmoothGlucose: Bool, fetchHours: Decimal) async throws -> [BloodGlucose] {
+        let context = makeContext()
+        let cutoff = Date().addingTimeInterval(-(Double(truncating: fetchHours as NSNumber) * 3600))
+
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "date >= %@", cutoff as NSDate),
+            key: "date",
+            ascending: false,
+            batchSize: 48
+        )
+
+        return try await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+
+            // extracting handler to only create it 1x
+            let roundingBehavior = NSDecimalNumberHandler(
+                roundingMode: .plain,
+                scale: 0,
+                raiseOnExactness: false,
+                raiseOnOverflow: false,
+                raiseOnUnderflow: false,
+                raiseOnDivideByZero: false
+            )
+
+            return glucoseResults.map {
+                Self.mapToBloodGlucose($0, shouldSmoothGlucose: shouldSmoothGlucose, roundingBehavior: roundingBehavior)
+            }
+        }
+    }
+
+    /// The glucose value the algorithm consumes for a stored reading: the smoothed value when
+    /// smoothing is on and a valid non-zero smoothed CGM value exists, otherwise the raw value
+    /// (finger pricks/manual entries always use the raw value — cf.
+    /// https://github.com/nightscout/Trio/issues/1054).
+    static func algorithmGlucoseValue(
+        for glucose: GlucoseStored,
+        shouldSmoothGlucose: Bool,
+        roundingBehavior: NSDecimalNumberHandler
+    ) -> Int16 {
+        if shouldSmoothGlucose {
+            if !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0 {
+                return smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
+            } else {
+                return glucose.glucose
+            }
+        } else {
+            return glucose.glucose
+        }
+    }
+
+    /// Maps a `GlucoseStored` to the `BloodGlucose` the algorithm consumes, reproducing exactly
+    /// the coercions the old `AlgorithmGlucose` → JSON → `JSONBridge.glucose` round-trip applied:
+    /// - CGM readings populate `sgv`, manual readings populate `glucose` (driven by `isManual`).
+    /// - `dateString` is the stored reading date. The old path round-tripped it through an ISO8601
+    ///   fractional-seconds string, truncating it to millisecond precision; we keep the full-precision
+    ///   `Date` instead, since the extra sub-millisecond precision is harmless to the algorithm and
+    ///   the JSON representation is unaffected (`dateString` still encodes to 3-digit-ms ISO8601).
+    /// - `date` is the unix-millisecond timestamp as a `Decimal`.
+    /// - `type` is always "sgv" (matching the old hardcoded encode).
+    static func mapToBloodGlucose(
+        _ glucose: GlucoseStored,
+        shouldSmoothGlucose: Bool,
+        roundingBehavior: NSDecimalNumberHandler
+    ) -> BloodGlucose {
+        let glucoseValue = algorithmGlucoseValue(
+            for: glucose,
+            shouldSmoothGlucose: shouldSmoothGlucose,
+            roundingBehavior: roundingBehavior
+        )
+        let isManual = glucose.isManual
+        // The ?? Date() is problematic, but this is how it worked previously
+        // so we'll retain it
+        let dateString = glucose.date ?? Date()
+        let dateMilliseconds = Decimal(Int64(dateString.timeIntervalSince1970 * 1000))
+
+        return BloodGlucose(
+            id: glucose.id?.uuidString ?? UUID().uuidString,
+            sgv: isManual ? nil : Int(glucoseValue),
+            direction: glucose.direction.flatMap { BloodGlucose.Direction(rawValue: $0) },
+            date: dateMilliseconds,
+            dateString: dateString,
+            glucose: isManual ? Int(glucoseValue) : nil,
+            type: "sgv"
+        )
+    }
+
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {

+ 7 - 137
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -121601,6 +121601,9 @@
         }
       }
     },
+    "End Snooze" : {
+
+    },
     "Endpoint" : {
       "localizations" : {
         "bg" : {
@@ -260220,6 +260223,9 @@
         }
       }
     },
+    "Swipe left to end snooze." : {
+
+    },
     "Swipe the chart to scroll through time." : {
       "localizations" : {
         "bg" : {
@@ -260593,9 +260599,6 @@
         }
       }
     },
-    "Switches back to the legacy JavaScript-based algorithm version." : {
-      "comment" : "Use JavaScript Oref mini hint"
-    },
     "Synth" : {
       "comment" : "Display name for the synth alarm sound.",
       "localizations" : {
@@ -292551,9 +292554,6 @@
         }
       }
     },
-    "Trio now uses a fully Swift-based version of the algorithm (Oref) by default. It's faster, more accurate and improves Trio for everyone." : {
-
-    },
     "Trio Personalization" : {
       "localizations" : {
         "bg" : {
@@ -302333,9 +302333,6 @@
         }
       }
     },
-    "Use JavaScript Oref" : {
-      "comment" : "Use JavaScript Oref"
-    },
     "Use local glucose server" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -310866,134 +310863,6 @@
         }
       }
     },
-    "When enabled, Trio will instead use the legacy JavaScript-based algorithm that runs virtualized on your phone. Only enable this if you encounter issues with the Swift-based algorithm." : {
-
-    },
-    "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm." : {
-      "extractionState" : "stale",
-      "localizations" : {
-        "bg" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "cs" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "da" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "de" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "es" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "fr" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "he" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "it" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "ko" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "nb-NO" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Når den er aktivert, vil Trio ikke lenger bruke den gamle JavaScript-baserte algoritmen som kjører virtualisert på telefonen din. I stedet vil den bruke en fullstendig Swift-basert algoritme."
-          }
-        },
-        "nl" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "pl" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "pt-PT" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "ro" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "ru" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "sv" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "tr" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "uk" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "vi" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        },
-        "zh-Hant" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "When enabled, Trio will no longer use the old JavaScript-based algorithm that runs virtualized on your phone. Instead, it will use a fully Swift-based algorithm."
-          }
-        }
-      }
-    },
     "When Remote Control is enabled, you can send boluses, overrides, temporary targets, carbs, and other commands to Trio via push notifications." : {
       "localizations" : {
         "bg" : {
@@ -315274,6 +315143,7 @@
       }
     },
     "You can disable this feature anytime." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 0 - 86
Trio/Sources/Models/AlgorithmGlucose.swift

@@ -1,86 +0,0 @@
-import Foundation
-
-/// Helper class so that we can have a plain Swift object to serialize GlucoseStorage
-struct AlgorithmGlucose: Codable {
-    var date: Date?
-    var direction: String?
-    var glucose: Int16
-    var id: UUID?
-    var isManual: Bool
-
-    enum CodingKeys: String, CodingKey {
-        case date
-        case dateString
-        case sgv
-        case glucose
-        case direction
-        case id
-        case type
-    }
-
-    init(date: Date?, direction: String?, glucose: Int16, id: UUID?, isManual: Bool) {
-        self.date = date
-        self.direction = direction
-        self.glucose = glucose
-        self.id = id
-        self.isManual = isManual
-    }
-
-    // this constructor is just for testing
-    public init(from decoder: Decoder) throws {
-        let container = try decoder.container(keyedBy: CodingKeys.self)
-
-        if let dateString = try container.decodeIfPresent(String.self, forKey: .dateString) {
-            let dateFormatter = ISO8601DateFormatter()
-            dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-            date = dateFormatter.date(from: dateString)
-        } else if let dateStringTimestamp = try container.decodeIfPresent(String.self, forKey: .date),
-                  let dateTimestamp = TimeInterval(dateStringTimestamp)
-        {
-            date = Date(timeIntervalSince1970: dateTimestamp / 1000)
-        } else {
-            date = nil
-        }
-
-        direction = try container.decodeIfPresent(String.self, forKey: .direction)
-        id = try container.decodeIfPresent(UUID.self, forKey: .id)
-
-        if let glucoseValue = try container.decodeIfPresent(Int16.self, forKey: .glucose) {
-            glucose = glucoseValue
-            isManual = true
-        } else if let sgvValue = try container.decodeIfPresent(Int16.self, forKey: .sgv) {
-            glucose = sgvValue
-            isManual = false
-        } else {
-            throw DecodingError.dataCorruptedError(
-                forKey: .sgv,
-                in: container,
-                debugDescription: "Neither 'glucose' nor 'sgv' key found or value is not Int16"
-            )
-        }
-    }
-
-    public func encode(to encoder: Encoder) throws {
-        var container = encoder.container(keyedBy: CodingKeys.self)
-
-        let dateFormatter = ISO8601DateFormatter()
-        dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-
-        try container.encode(dateFormatter.string(from: date ?? Date()), forKey: .dateString)
-
-        let dateAsUnixTimestamp = Int64((date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
-        try container.encode(dateAsUnixTimestamp, forKey: .date)
-
-        try container.encode(direction, forKey: .direction)
-        try container.encode(id, forKey: .id)
-
-        // TODO: Handle the type of the glucose entry conditionally not hardcoded
-        try container.encode("sgv", forKey: .type)
-
-        if isManual {
-            try container.encode(glucose, forKey: .glucose)
-        } else {
-            try container.encode(glucose, forKey: .sgv)
-        }
-    }
-}

+ 5 - 0
Trio/Sources/Models/BloodGlucose.swift

@@ -173,6 +173,11 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
     var transmitterID: String? = nil
     var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
 
+    // TODO: remove this custom Equatable/Hashable. Keying identity on `dateString` is a footgun:
+    // two distinct readings at the same instant collide, and the same reading at different date
+    // precision compares unequal. `id` (the sync identifier) is the natural key. It's currently
+    // safe to leave — nothing live compares `BloodGlucose` via ==/hash (only the unused
+    // `History.Glucose` reaches it) — but it should be removed in its own change.
     static func == (lhs: BloodGlucose, rhs: BloodGlucose) -> Bool {
         lhs.dateString == rhs.dateString
     }

+ 0 - 4
Trio/Sources/Models/Determination.swift

@@ -1,9 +1,5 @@
 import Foundation
 
-struct DeterminationErrorResponse: JSON, Equatable {
-    let error: String
-}
-
 struct Determination: JSON, Equatable {
     let id: UUID?
     var reason: String

+ 0 - 5
Trio/Sources/Models/TrioSettings.swift

@@ -70,7 +70,6 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
     var requireAdjustmentsConfirmation: Bool = false
-    var useJavascriptOref: Bool = false
 
     /// Selected Garmin watchface (Trio or SwissAlpine)
     var garminWatchface: GarminWatchface = .trio
@@ -328,10 +327,6 @@ extension TrioSettings: Decodable {
             settings.requireAdjustmentsConfirmation = requireAdjustmentsConfirmation
         }
 
-        if let useJavascriptOref = try? container.decode(Bool.self, forKey: .useJavascriptOref) {
-            settings.useJavascriptOref = useJavascriptOref
-        }
-
         if let garminWatchface = try? container.decode(GarminWatchface.self, forKey: .garminWatchface) {
             settings.garminWatchface = garminWatchface
         }

+ 0 - 3
Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -22,7 +22,6 @@ extension AlgorithmAdvancedSettings {
         @Published var remainingCarbsFraction: Decimal = 1.0
         @Published var remainingCarbsCap: Decimal = 90
         @Published var noisyCGMTargetMultiplier: Decimal = 1.3
-        @Published var useJavascriptOref: Bool = false
         // preference
         @Published var insulinActionCurve: Decimal = 10
         @Published var smbDeliveryRatio: Decimal = 0.5
@@ -48,8 +47,6 @@ extension AlgorithmAdvancedSettings {
             subscribePreferencesSetting(\.remainingCarbsCap, on: $remainingCarbsCap) { remainingCarbsCap = $0 }
             subscribePreferencesSetting(\.noisyCGMTargetMultiplier, on: $noisyCGMTargetMultiplier) {
                 noisyCGMTargetMultiplier = $0 }
-            subscribeSetting(\.useJavascriptOref, on: $useJavascriptOref) {
-                useJavascriptOref = $0 }
             subscribePreferencesSetting(\.smbDeliveryRatio, on: $smbDeliveryRatio) { smbDeliveryRatio = $0 }
             subscribePreferencesSetting(\.smbInterval, on: $smbInterval) { smbInterval = $0 }
             subscribePreferencesSetting(\.smbDeliveryRatio, on: $smbDeliveryRatio) { smbDeliveryRatio = $0 }

+ 0 - 33
Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -390,39 +390,6 @@ extension AlgorithmAdvancedSettings {
                         Text("Note: A CGM is considered noisy when it provides inconsistent readings.")
                     }
                 )
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useJavascriptOref,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = NSLocalizedString("Use JavaScript Oref", comment: "Use JavaScript Oref")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: NSLocalizedString("Use JavaScript Oref", comment: "Use JavaScript Oref"),
-                    miniHint: String(
-                        localized: "Switches back to the legacy JavaScript-based algorithm version.",
-                        comment: "Use JavaScript Oref mini hint"
-                    ),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text(
-                            "Trio now uses a fully Swift-based version of the algorithm (Oref) by default. It's faster, more accurate and improves Trio for everyone."
-                        )
-                        Text(
-                            "When enabled, Trio will instead use the legacy JavaScript-based algorithm that runs virtualized on your phone. Only enable this if you encounter issues with the Swift-based algorithm."
-                        )
-
-                        Text(
-                            "You can disable this feature anytime."
-                        )
-                    }
-                )
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 0 - 5
Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift

@@ -288,11 +288,6 @@ struct LoopStatusView: View {
             tags.append("Smoothing: On")
         }
 
-        // FIXME: remove this before feat/dev-oref-swift is merged to dev
-        if !state.settingsManager.settings.useJavascriptOref {
-            tags.append("Swift Oref")
-        }
-
         if let currentTDD = state.fetchedTDDs.first?.totalDailyDose, currentTDD != 0 {
             tags.append("TDD: \(currentTDD)")
         }

+ 292 - 0
TrioTests/CarbsNativeConversionTests.swift

@@ -0,0 +1,292 @@
+import CoreData
+import Foundation
+import Testing
+
+@testable import Trio
+
+/// Golden tests certifying that the native `CarbEntryStored` → `CarbsEntry` mapping
+/// (`BaseCarbsStorage.mapToCarbsEntry`) reproduces, field for field, the carbs the algorithm used to receive
+/// through the old JSON round-trip (`CarbEntryStored` → JSON → `JSONBridge.carbs`) — with one
+/// deliberate, documented change: `id`.
+@Suite("Carbs Native Conversion Tests", .serialized) struct CarbsNativeConversionTests {
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
+
+    init() async throws {
+        coreDataStack = try await CoreDataStack.createForTests()
+        testContext = coreDataStack.newTaskContext()
+    }
+
+    // MARK: - Golden tests (native mapping vs frozen old-path output)
+
+    @Test("A basic carb entry maps identically (id now populated)") func testBasicEntry() async throws {
+        await insertCarb(carbs: 30, isFPU: false, date: fixedDate(minutesAgo: 0), note: "breakfast", id: uuid(1))
+
+        try await assertNativeMatchesGolden([
+            CarbsEntry(
+                id: uuid(1).uuidString,
+                createdAt: Date(timeIntervalSince1970: 1_700_000_000),
+                actualDate: Date(timeIntervalSince1970: 1_700_000_000),
+                carbs: 30,
+                fat: 0,
+                protein: 0,
+                note: nil, // old path decoded "note" as "notes" → nil; preserved
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: nil
+            )
+        ])
+    }
+
+    @Test("Fractional carbs/fat/protein keep clean decimal values") func testFractionalDecimals() async throws {
+        // 33.33 as a Double is 33.32999999999999488; the old JSON round-trip recovered the clean
+        // 33.33 (JSONEncoder writes the shortest round-trippable string). `Decimal(Double)` would
+        // leak the binary expansion, so `Decimal(algorithmValue:)` must reproduce the clean value.
+        await insertCarb(carbs: 33.33, isFPU: true, date: fixedDate(minutesAgo: 0), fat: 5.5, protein: 3.2, id: uuid(1))
+        await insertCarb(carbs: 12.5, isFPU: false, date: fixedDate(minutesAgo: 5), id: uuid(2))
+
+        try await assertNativeMatchesGolden([
+            CarbsEntry(
+                id: uuid(1).uuidString,
+                createdAt: Date(timeIntervalSince1970: 1_700_000_000),
+                actualDate: Date(timeIntervalSince1970: 1_700_000_000),
+                carbs: Decimal(string: "33.33")!,
+                fat: Decimal(string: "5.5")!,
+                protein: Decimal(string: "3.2")!,
+                note: nil,
+                enteredBy: CarbsEntry.local,
+                isFPU: true,
+                fpuID: nil
+            ),
+            CarbsEntry(
+                id: uuid(2).uuidString,
+                createdAt: Date(timeIntervalSince1970: 1_699_999_700),
+                actualDate: Date(timeIntervalSince1970: 1_699_999_700),
+                carbs: Decimal(string: "12.5")!,
+                fat: 0,
+                protein: 0,
+                note: nil,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: nil
+            )
+        ])
+
+        let native = try await nativeCarbsEntries()
+        #expect(native.first?.carbs == Decimal(string: "33.33")!, "33.33 must stay clean, not the Double expansion")
+    }
+
+    @Test("A zero-carb entry (nil stored id) maps identically") func testZeroCarbNilId() async throws {
+        // The old path always produced id == nil, so a stored entry with no id is indistinguishable
+        // there. Natively, a nil stored id still maps to a nil `CarbsEntry.id`.
+        await insertCarb(carbs: 0, isFPU: false, date: fixedDate(minutesAgo: 0), id: nil)
+
+        try await assertNativeMatchesGolden([
+            CarbsEntry(
+                id: nil,
+                createdAt: Date(timeIntervalSince1970: 1_700_000_000),
+                actualDate: Date(timeIntervalSince1970: 1_700_000_000),
+                carbs: 0,
+                fat: 0,
+                protein: 0,
+                note: nil,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: nil
+            )
+        ])
+    }
+
+    @Test("A multi-entry sequence maps identically") func testMultiEntrySequence() async throws {
+        for i in 0 ..< 5 {
+            await insertCarb(
+                carbs: Double(10 * (i + 1)),
+                isFPU: false,
+                date: fixedDate(minutesAgo: Double(i) * 5),
+                id: uuid(i + 1)
+            )
+        }
+
+        try await assertNativeMatchesGolden((0 ..< 5).map { i in
+            CarbsEntry(
+                id: uuid(i + 1).uuidString,
+                createdAt: Date(timeIntervalSince1970: 1_700_000_000 - Double(i) * 300),
+                actualDate: Date(timeIntervalSince1970: 1_700_000_000 - Double(i) * 300),
+                carbs: Decimal(10 * (i + 1)),
+                fat: 0,
+                protein: 0,
+                note: nil,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: nil
+            )
+        })
+    }
+
+    @Test("Sub-millisecond dates are truncated to millisecond resolution") func testMillisecondTruncation() async throws {
+        // The old path round-tripped the stored date through an ISO8601 fractional-seconds string
+        // (millisecond precision). The native path keeps the full-precision `Date`, but only
+        // millisecond precision is ever observable, so the comparison is at ms resolution.
+        await insertCarb(carbs: 25, isFPU: false, date: fixedDate(minutesAgo: 0, plusSeconds: 0.123_456), id: uuid(1))
+
+        try await assertNativeMatchesGolden([
+            CarbsEntry(
+                id: uuid(1).uuidString,
+                createdAt: Date(timeIntervalSince1970: 1_700_000_000.123),
+                actualDate: Date(timeIntervalSince1970: 1_700_000_000.123),
+                carbs: 25,
+                fat: 0,
+                protein: 0,
+                note: nil,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: nil
+            )
+        ])
+    }
+
+    // MARK: - The one deliberate behavioral change
+
+    @Test("id is now populated from Core Data (old path always produced nil)") func testIdIsNowPopulatedFromCoreData() async throws {
+        await insertCarb(carbs: 40, isFPU: false, date: fixedDate(minutesAgo: 0), id: uuid(7))
+
+        let native = try await nativeCarbsEntries()
+        // Old JSON path: `id` was encoded under "id" but decoded from "_id", so this was always nil.
+        // The native mapping carries the real Core Data id instead.
+        #expect(native.first?.id == uuid(7).uuidString, "native mapping must carry the Core Data id")
+    }
+
+    // MARK: - Additional (synthetic) carbs entry
+
+    @Test("The synthetic additional-carbs entry matches the old spliced dictionary") func testAdditionalCarbsEntry() {
+        let date = Date(timeIntervalSince1970: 1_700_000_000)
+        let id = uuid(99).uuidString
+        let entry = BaseCarbsStorage.additionalCarbsEntry(carbs: 15, date: date, id: id)
+
+        // Old path spliced a dictionary that decoded to: id=nil (encoded "id", decoded "_id"),
+        // note=nil, fat=0, protein=0, isFPU=false, enteredBy="Trio", both dates == the passed date.
+        // The only intended difference is `id`, which we now carry (see `additionalCarbsEntry`).
+        let expected = CarbsEntry(
+            id: id,
+            createdAt: date,
+            actualDate: date,
+            carbs: 15,
+            fat: 0,
+            protein: 0,
+            note: nil,
+            enteredBy: CarbsEntry.local,
+            isFPU: false,
+            fpuID: nil
+        )
+        expectFieldsEqual(entry, expected, entry: 0)
+    }
+
+    @Test("A zero additional-carbs entry (the normal loop case) is well formed") func testAdditionalCarbsZero() {
+        // In the normal determine-basal loop `additionalCarbs` is `simulatedCarbsAmount ?? 0`, so a
+        // carbs=0 entry is always appended. MealHistory/AutosensGenerator drop carbs <= 0, so it is
+        // inert, but we still reproduce the old shape exactly.
+        let date = Date(timeIntervalSince1970: 1_700_000_000)
+        let entry = BaseCarbsStorage.additionalCarbsEntry(carbs: 0, date: date, id: uuid(1).uuidString)
+        #expect(entry.carbs == 0)
+        #expect(entry.isFPU == false)
+        #expect(entry.enteredBy == CarbsEntry.local)
+    }
+
+    // MARK: - Comparison helpers
+
+    /// Asserts the native mapping reproduces the frozen golden `CarbsEntry` values.
+    private func assertNativeMatchesGolden(_ golden: [CarbsEntry]) async throws {
+        let native = try await nativeCarbsEntries()
+
+        #expect(native.count == golden.count, "native produced \(native.count) entries, golden has \(golden.count)")
+
+        for (index, pair) in zip(native, golden).enumerated() {
+            expectFieldsEqual(pair.0, pair.1, entry: index)
+        }
+    }
+
+    /// Field-by-field comparison
+    private func expectFieldsEqual(_ actual: CarbsEntry, _ expected: CarbsEntry, entry index: Int) {
+        #expect(actual.id == expected.id, "entry \(index): id \(actual.id ?? "nil") != \(expected.id ?? "nil")")
+        #expect(
+            Self.millisecondString(actual.createdAt) == Self.millisecondString(expected.createdAt),
+            "entry \(index): createdAt \(Self.millisecondString(actual.createdAt)) != \(Self.millisecondString(expected.createdAt))"
+        )
+        #expect(
+            actual.actualDate.map(Self.millisecondString) == expected.actualDate.map(Self.millisecondString),
+            "entry \(index): actualDate mismatch"
+        )
+        #expect(actual.carbs == expected.carbs, "entry \(index): carbs \(actual.carbs) != \(expected.carbs)")
+        #expect(
+            actual.fat == expected.fat,
+            "entry \(index): fat \(String(describing: actual.fat)) != \(String(describing: expected.fat))"
+        )
+        #expect(
+            actual.protein == expected.protein,
+            "entry \(index): protein \(String(describing: actual.protein)) != \(String(describing: expected.protein))"
+        )
+        #expect(actual.note == expected.note, "entry \(index): note \(actual.note ?? "nil") != \(expected.note ?? "nil")")
+        #expect(
+            actual.enteredBy == expected.enteredBy,
+            "entry \(index): enteredBy \(actual.enteredBy ?? "nil") != \(expected.enteredBy ?? "nil")"
+        )
+        #expect(
+            actual.isFPU == expected.isFPU,
+            "entry \(index): isFPU \(String(describing: actual.isFPU)) != \(String(describing: expected.isFPU))"
+        )
+        #expect(actual.fpuID == expected.fpuID, "entry \(index): fpuID \(actual.fpuID ?? "nil") != \(expected.fpuID ?? "nil")")
+    }
+
+    private static func millisecondString(_ date: Date) -> String {
+        Formatter.iso8601withFractionalSeconds.string(from: date)
+    }
+
+    private func nativeCarbsEntries() async throws -> [CarbsEntry] {
+        try await testContext.perform {
+            try self.fetchRowsNewestFirst().map { BaseCarbsStorage.mapToCarbsEntry($0) }
+        }
+    }
+
+    /// Must be called from within `testContext.perform`. Mirrors the `date`-descending order the
+    /// production fetch uses (`BaseCarbsStorage.getCarbsForAlgorithm`).
+    private func fetchRowsNewestFirst() throws -> [CarbEntryStored] {
+        let request = CarbEntryStored.fetchRequest()
+        request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
+        return try testContext.fetch(request)
+    }
+
+    // MARK: - Fixture helpers
+
+    private func insertCarb(
+        carbs: Double,
+        isFPU: Bool,
+        date: Date,
+        fat: Double = 0,
+        protein: Double = 0,
+        note: String? = nil,
+        id: UUID?,
+        fpuID: UUID? = nil
+    ) async {
+        await testContext.perform {
+            let object = CarbEntryStored(context: self.testContext)
+            object.carbs = carbs
+            object.isFPU = isFPU
+            object.date = date
+            object.fat = fat
+            object.protein = protein
+            object.note = note
+            object.id = id
+            object.fpuID = fpuID
+            try! self.testContext.save()
+        }
+    }
+
+    /// A fixed base timestamp (2023-11-14T22:13:20Z) so fixtures are deterministic and reproducible.
+    private func fixedDate(minutesAgo: Double, plusSeconds: Double = 0) -> Date {
+        Date(timeIntervalSince1970: 1_700_000_000 + plusSeconds - minutesAgo * 60)
+    }
+
+    private func uuid(_ n: Int) -> UUID {
+        UUID(uuidString: String(format: "00000000-0000-0000-0000-%012d", n))!
+    }
+}

+ 461 - 0
TrioTests/GlucoseNativeConversionTests.swift

@@ -0,0 +1,461 @@
+import CoreData
+import Foundation
+import Testing
+
+@testable import Trio
+
+/// Golden tests certifying that the native `GlucoseStored` → `BloodGlucose` mapping
+/// (`BaseGlucoseStorage.mapToBloodGlucose`) reproduces, byte for byte, the glucose the algorithm used to
+/// receive through the old JSON round-trip (`GlucoseStored` → `AlgorithmGlucose` → JSON →
+/// `JSONBridge.glucose`). The golden literals below were captured from that old path while it
+/// still existed (via a temporary differential run), so a match proves the algorithm still sees
+/// identical inputs now that the JSON round-trip is gone.
+///
+/// The comparison is field by field (see `expectFieldsEqual`), NOT `==`: `BloodGlucose.==` only
+/// compares `dateString`, so a plain array comparison would pass even if
+/// `glucose`/`sgv`/`direction`/`id`/`type` differed — exactly the coerced fields this migration
+/// must preserve.
+///
+/// Fixtures use fixed dates and ids so the mapping is fully deterministic and independent of the
+/// wall-clock-relative time-window fetch (which is unchanged by this migration and stays covered
+/// by `GlucoseSmoothingTests`).
+@Suite("Glucose Native Conversion Tests", .serialized) struct GlucoseNativeConversionTests {
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
+
+    init() async throws {
+        coreDataStack = try await CoreDataStack.createForTests()
+        testContext = coreDataStack.newTaskContext()
+    }
+
+    // MARK: - Golden tests (native mapping vs frozen old-path output)
+
+    @Test("CGM readings without smoothing map identically") func testCGMNoSmoothing() async throws {
+        await insertGlucose(glucose: 120, isManual: false, date: fixedDate(minutesAgo: 0), direction: "Flat", id: uuid(1))
+        await insertGlucose(glucose: 95, isManual: false, date: fixedDate(minutesAgo: 5), direction: "FortyFiveDown", id: uuid(2))
+        await insertGlucose(glucose: 150, isManual: true, date: fixedDate(minutesAgo: 10), direction: nil, id: uuid(3))
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 120,
+                direction: .flat,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(2).uuidString,
+                sgv: 95,
+                direction: .fortyFiveDown,
+                date: 1_699_999_700_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_700),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(3).uuidString,
+                date: 1_699_999_400_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_400),
+                glucose: 150,
+                type: "sgv"
+            )
+        ])
+    }
+
+    @Test("Smoothed CGM value is used and rounded identically") func testSmoothedValueUsed() async throws {
+        await insertGlucose(
+            glucose: 120,
+            isManual: false,
+            date: fixedDate(minutesAgo: 0),
+            smoothed: Decimal(string: "118.6"),
+            direction: "Flat",
+            id: uuid(1)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 119,
+                direction: .flat,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            )
+        ])
+
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
+        #expect(native.first?.sgv == 119, "118.6 should round to 119 in the sgv field")
+        #expect(native.first?.glucose == nil, "CGM readings must not populate the manual `glucose` field")
+    }
+
+    @Test("Zero smoothed value falls back to the raw value") func testSmoothedZeroFallsBackToRaw() async throws {
+        await insertGlucose(
+            glucose: 120,
+            isManual: false,
+            date: fixedDate(minutesAgo: 0),
+            smoothed: 0,
+            direction: "Flat",
+            id: uuid(1)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 120,
+                direction: .flat,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            )
+        ])
+
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
+        #expect(native.first?.sgv == 120, "A zero smoothed value must fall back to the raw 120")
+    }
+
+    @Test("Nil smoothed value falls back to the raw value") func testSmoothedNilFallsBackToRaw() async throws {
+        await insertGlucose(
+            glucose: 120,
+            isManual: false,
+            date: fixedDate(minutesAgo: 0),
+            smoothed: nil,
+            direction: "Flat",
+            id: uuid(1)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 120,
+                direction: .flat,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            )
+        ])
+
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
+        #expect(native.first?.sgv == 120, "A nil smoothed value must fall back to the raw 120")
+    }
+
+    @Test("Manual entries ignore smoothing and populate the glucose field") func testManualIgnoresSmoothing() async throws {
+        await insertGlucose(
+            glucose: 150,
+            isManual: true,
+            date: fixedDate(minutesAgo: 0),
+            smoothed: 140,
+            direction: nil,
+            id: uuid(1)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                glucose: 150,
+                type: "sgv"
+            )
+        ])
+
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
+        #expect(native.first?.glucose == 150, "Manual entries must use the raw value in the `glucose` field")
+        #expect(native.first?.sgv == nil, "Manual entries must not populate the `sgv` field")
+    }
+
+    @Test("Direction variants and nil map identically") func testDirectionVariants() async throws {
+        await insertGlucose(glucose: 110, isManual: false, date: fixedDate(minutesAgo: 0), direction: "TripleUp", id: uuid(1))
+        await insertGlucose(glucose: 111, isManual: false, date: fixedDate(minutesAgo: 5), direction: "FortyFiveUp", id: uuid(2))
+        await insertGlucose(glucose: 112, isManual: false, date: fixedDate(minutesAgo: 10), direction: "NONE", id: uuid(3))
+        await insertGlucose(glucose: 113, isManual: false, date: fixedDate(minutesAgo: 15), direction: nil, id: uuid(4))
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 110,
+                direction: .tripleUp,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(2).uuidString,
+                sgv: 111,
+                direction: .fortyFiveUp,
+                date: 1_699_999_700_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_700),
+                type: "sgv"
+            ),
+            // `.none` here is BloodGlucose.Direction.none ("NONE"), not Optional.none — spell it out.
+            BloodGlucose(
+                id: uuid(3).uuidString,
+                sgv: 112,
+                direction: BloodGlucose.Direction.none,
+                date: 1_699_999_400_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_400),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(4).uuidString,
+                sgv: 113,
+                date: 1_699_999_100_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_100),
+                type: "sgv"
+            )
+        ])
+    }
+
+    @Test("Sub-millisecond dates are truncated identically") func testMillisecondTruncation() async throws {
+        // A date with sub-millisecond precision: the old path round-trips it through an ISO8601
+        // fractional-seconds string (millisecond precision), so both fields must be ms-truncated.
+        await insertGlucose(
+            glucose: 100,
+            isManual: false,
+            date: fixedDate(minutesAgo: 0, plusSeconds: 0.123_456),
+            direction: "Flat",
+            id: uuid(1)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 100,
+                direction: .flat,
+                date: 1_700_000_000_123,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000.123),
+                type: "sgv"
+            )
+        ])
+    }
+
+    @Test("A descending multi-entry sequence maps identically") func testDescendingSequence() async throws {
+        for i in 0 ..< 6 {
+            await insertGlucose(
+                glucose: Int16(100 + i),
+                isManual: false,
+                date: fixedDate(minutesAgo: Double(i) * 5),
+                direction: "Flat",
+                id: uuid(i + 1)
+            )
+        }
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 100,
+                direction: .flat,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(2).uuidString,
+                sgv: 101,
+                direction: .flat,
+                date: 1_699_999_700_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_700),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(3).uuidString,
+                sgv: 102,
+                direction: .flat,
+                date: 1_699_999_400_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_400),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(4).uuidString,
+                sgv: 103,
+                direction: .flat,
+                date: 1_699_999_100_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_100),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(5).uuidString,
+                sgv: 104,
+                direction: .flat,
+                date: 1_699_998_800_000,
+                dateString: Date(timeIntervalSince1970: 1_699_998_800),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(6).uuidString,
+                sgv: 105,
+                direction: .flat,
+                date: 1_699_998_500_000,
+                dateString: Date(timeIntervalSince1970: 1_699_998_500),
+                type: "sgv"
+            )
+        ])
+    }
+
+    @Test("Smoothing rounding boundaries map identically") func testRoundingBoundaries() async throws {
+        await insertGlucose(
+            glucose: 100,
+            isManual: false,
+            date: fixedDate(minutesAgo: 0),
+            smoothed: Decimal(string: "118.5"),
+            id: uuid(1)
+        )
+        await insertGlucose(
+            glucose: 100,
+            isManual: false,
+            date: fixedDate(minutesAgo: 5),
+            smoothed: Decimal(string: "118.4"),
+            id: uuid(2)
+        )
+        await insertGlucose(
+            glucose: 100,
+            isManual: false,
+            date: fixedDate(minutesAgo: 10),
+            smoothed: Decimal(string: "119.5"),
+            id: uuid(3)
+        )
+
+        try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
+            BloodGlucose(
+                id: uuid(1).uuidString,
+                sgv: 119,
+                date: 1_700_000_000_000,
+                dateString: Date(timeIntervalSince1970: 1_700_000_000),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(2).uuidString,
+                sgv: 118,
+                date: 1_699_999_700_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_700),
+                type: "sgv"
+            ),
+            BloodGlucose(
+                id: uuid(3).uuidString,
+                sgv: 120,
+                date: 1_699_999_400_000,
+                dateString: Date(timeIntervalSince1970: 1_699_999_400),
+                type: "sgv"
+            )
+        ])
+
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
+        #expect(native.map(\.sgv) == [119, 118, 120], ".plain scale-0 rounding: 118.5→119, 118.4→118, 119.5→120")
+    }
+
+    // MARK: - Comparison helpers
+
+    /// Asserts the native mapping reproduces the frozen golden `BloodGlucose` values. The goldens
+    /// were captured from the old `AlgorithmGlucose` → JSON → `JSONBridge.glucose` path (see the
+    /// differential run in this migration's history), so a match proves the algorithm still receives
+    /// identical glucose after the JSON round-trip was removed.
+    private func assertNativeMatchesGolden(shouldSmoothGlucose: Bool, _ golden: [BloodGlucose]) async throws {
+        let native = try await nativeBloodGlucose(shouldSmoothGlucose: shouldSmoothGlucose)
+
+        #expect(native.count == golden.count, "native produced \(native.count) entries, golden has \(golden.count)")
+
+        for (index, pair) in zip(native, golden).enumerated() {
+            expectFieldsEqual(pair.0, pair.1, entry: index)
+        }
+    }
+
+    /// Field-by-field comparison. We can't use `==`: `BloodGlucose.==` only compares `dateString`,
+    /// so a direct comparison would pass even if `sgv`/`glucose`/`direction`/`id`/`type` differed —
+    /// exactly the coerced fields we must pin.
+    ///
+    /// `dateString` is compared at millisecond resolution rather than as an exact `Date`. The mapping
+    /// keeps the reading's full-precision date in memory, but only millisecond precision is ever
+    /// observable — it's what `date` pins and what the value serializes to — and Core Data's `Double`
+    /// round-trip perturbs sub-millisecond bits anyway, so an exact `Date` comparison would be flaky.
+    private func expectFieldsEqual(_ actual: BloodGlucose, _ expected: BloodGlucose, entry index: Int) {
+        #expect(actual.id == expected.id, "entry \(index): id \(actual.id) != \(expected.id)")
+        #expect(actual.legacyId == expected.legacyId, "entry \(index): legacyId")
+        #expect(
+            actual.sgv == expected.sgv,
+            "entry \(index): sgv \(actual.sgv.map(String.init) ?? "nil") != \(expected.sgv.map(String.init) ?? "nil")"
+        )
+        #expect(
+            actual.glucose == expected.glucose,
+            "entry \(index): glucose \(actual.glucose.map(String.init) ?? "nil") != \(expected.glucose.map(String.init) ?? "nil")"
+        )
+        #expect(actual.mbg == expected.mbg, "entry \(index): mbg")
+        #expect(
+            actual.direction == expected.direction,
+            "entry \(index): direction \(actual.direction?.rawValue ?? "nil") != \(expected.direction?.rawValue ?? "nil")"
+        )
+        #expect(actual.date == expected.date, "entry \(index): date \(actual.date) != \(expected.date)")
+        #expect(
+            Self.millisecondString(actual.dateString) == Self.millisecondString(expected.dateString),
+            "entry \(index): dateString \(Self.millisecondString(actual.dateString)) != \(Self.millisecondString(expected.dateString))"
+        )
+        #expect(actual.type == expected.type, "entry \(index): type \(actual.type ?? "nil") != \(expected.type ?? "nil")")
+        #expect(actual.unfiltered == expected.unfiltered, "entry \(index): unfiltered")
+        #expect(actual.filtered == expected.filtered, "entry \(index): filtered")
+        #expect(actual.noise == expected.noise, "entry \(index): noise")
+        #expect(actual.activationDate == expected.activationDate, "entry \(index): activationDate")
+        #expect(actual.sessionStartDate == expected.sessionStartDate, "entry \(index): sessionStartDate")
+        #expect(actual.transmitterID == expected.transmitterID, "entry \(index): transmitterID")
+    }
+
+    private static func millisecondString(_ date: Date) -> String {
+        Formatter.iso8601withFractionalSeconds.string(from: date)
+    }
+
+    private func nativeBloodGlucose(shouldSmoothGlucose: Bool) async throws -> [BloodGlucose] {
+        try await testContext.perform {
+            try self.fetchRowsNewestFirst().map {
+                BaseGlucoseStorage.mapToBloodGlucose(
+                    $0,
+                    shouldSmoothGlucose: shouldSmoothGlucose,
+                    roundingBehavior: Self.roundingBehavior
+                )
+            }
+        }
+    }
+
+    /// Must be called from within `testContext.perform`.
+    private func fetchRowsNewestFirst() throws -> [GlucoseStored] {
+        let request = GlucoseStored.fetchRequest()
+        request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
+        return try testContext.fetch(request)
+    }
+
+    private static let roundingBehavior = NSDecimalNumberHandler(
+        roundingMode: .plain,
+        scale: 0,
+        raiseOnExactness: false,
+        raiseOnOverflow: false,
+        raiseOnUnderflow: false,
+        raiseOnDivideByZero: false
+    )
+
+    // MARK: - Fixture helpers
+
+    private func insertGlucose(
+        glucose: Int16,
+        isManual: Bool,
+        date: Date,
+        smoothed: Decimal? = nil,
+        direction: String? = nil,
+        id: UUID
+    ) async {
+        await testContext.perform {
+            let object = GlucoseStored(context: self.testContext)
+            object.glucose = glucose
+            object.isManual = isManual
+            object.date = date
+            object.smoothedGlucose = smoothed.map { NSDecimalNumber(decimal: $0) }
+            object.direction = direction
+            object.id = id
+            try! self.testContext.save()
+        }
+    }
+
+    /// A fixed base timestamp (2023-11-14T22:13:20Z) so fixtures are deterministic and reproducible.
+    private func fixedDate(minutesAgo: Double, plusSeconds: Double = 0) -> Date {
+        Date(timeIntervalSince1970: 1_700_000_000 + plusSeconds - minutesAgo * 60)
+    }
+
+    private func uuid(_ n: Int) -> UUID {
+        UUID(uuidString: String(format: "00000000-0000-0000-0000-%012d", n))!
+    }
+}

+ 13 - 31
TrioTests/GlucoseSmoothingTests.swift

@@ -11,7 +11,7 @@ import Testing
     var coreDataStack: CoreDataStack!
     var testContext: NSManagedObjectContext!
     var fetchGlucoseManager: BaseFetchGlucoseManager!
-    var openAPS: OpenAPS!
+    var glucoseStorage: BaseGlucoseStorage!
 
     init() async throws {
         coreDataStack = try await CoreDataStack.createForTests()
@@ -32,8 +32,8 @@ import Testing
 
         fetchGlucoseManager = resolver.resolve(FetchGlucoseManager.self)! as? BaseFetchGlucoseManager
 
-        let fileStorage = resolver.resolve(FileStorage.self)!
-        openAPS = OpenAPS(storage: fileStorage, tddStorage: MockTDDStorage())
+        let context: NSManagedObjectContext = testContext
+        glucoseStorage = BaseGlucoseStorage(resolver: resolver, contextProvider: { context })
     }
 
     // MARK: - Exponential Smoothing Tests
@@ -289,43 +289,43 @@ import Testing
     @Test("Algorithm uses smoothed glucose when enabled") func testAlgorithmUsesSmoothedGlucose() async throws {
         await createGlucose(glucose: 150, smoothed: 140, isManual: false, date: Date())
 
-        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+        let algorithmInput = try await glucoseStorage.getGlucoseForAlgorithm(shouldSmoothGlucose: true, fetchHours: 24)
 
         #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
         #expect(
-            algorithmInput.first?.glucose == 140,
-            "Algorithm should have used the smoothed glucose value (140), but used \(algorithmInput.first?.glucose ?? 0)."
+            algorithmInput.first?.sgv == 140,
+            "Algorithm should have used the smoothed glucose value (140), but used \(algorithmInput.first?.sgv ?? 0)."
         )
     }
 
     @Test("Algorithm uses raw glucose when smoothing is disabled") func testAlgorithmUsesRawGlucose() async throws {
         await createGlucose(glucose: 150, smoothed: 140, isManual: false, date: Date())
 
-        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: false)
+        let algorithmInput = try await glucoseStorage.getGlucoseForAlgorithm(shouldSmoothGlucose: false, fetchHours: 24)
 
         #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
         #expect(
-            algorithmInput.first?.glucose == 150,
-            "Algorithm should have used the raw glucose value (150), but used \(algorithmInput.first?.glucose ?? 0)."
+            algorithmInput.first?.sgv == 150,
+            "Algorithm should have used the raw glucose value (150), but used \(algorithmInput.first?.sgv ?? 0)."
         )
     }
 
     @Test("Algorithm falls back to raw glucose if smoothed value is missing") func testAlgorithmFallbackToRawGlucose() async throws {
         await createGlucose(glucose: 150, smoothed: nil, isManual: false, date: Date())
 
-        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+        let algorithmInput = try await glucoseStorage.getGlucoseForAlgorithm(shouldSmoothGlucose: true, fetchHours: 24)
 
         #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
         #expect(
-            algorithmInput.first?.glucose == 150,
-            "Algorithm should have fallen back to the raw glucose value (150), but used \(algorithmInput.first?.glucose ?? 0)."
+            algorithmInput.first?.sgv == 150,
+            "Algorithm should have fallen back to the raw glucose value (150), but used \(algorithmInput.first?.sgv ?? 0)."
         )
     }
 
     @Test("Algorithm ignores smoothed value for manual glucose entries") func testAlgorithmIgnoresSmoothedManualGlucose() async throws {
         await createGlucose(glucose: 150, smoothed: 140, isManual: true, date: Date())
 
-        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+        let algorithmInput = try await glucoseStorage.getGlucoseForAlgorithm(shouldSmoothGlucose: true, fetchHours: 24)
 
         #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
         #expect(
@@ -336,24 +336,6 @@ import Testing
 
     // MARK: - Helpers
 
-    private func runFetchAndProcessGlucose(smoothGlucose: Bool) async throws -> [AlgorithmGlucose] {
-        let jsonString = try await openAPS.fetchAndProcessGlucose(
-            context: testContext,
-            shouldSmoothGlucose: smoothGlucose,
-            fetchLimit: 10
-        )
-
-        let data = jsonString.data(using: .utf8)!
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .custom { decoder in
-            let container = try decoder.singleValueContainer()
-            let dateDouble = try container.decode(Double.self)
-            return Date(timeIntervalSince1970: dateDouble / 1000)
-        }
-
-        return try decoder.decode([AlgorithmGlucose].self, from: data)
-    }
-
     private func createGlucose(glucose: Int16, smoothed: Decimal?, isManual: Bool, date: Date) async {
         await testContext.perform {
             let object = GlucoseStored(context: self.testContext)

+ 17 - 24
TrioTests/JSONImporterTests.swift

@@ -92,7 +92,7 @@ class BundleReference {}
         ) as? [PumpEventStored] ?? []
 
         let objectIds = allReadings.map(\.objectID)
-        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, orphanedResumes: [], from: context)
+        let parsedHistory = OpenAPS.nativePumpHistory(objectIds, orphanedResumes: [], from: context)
 
         var bolusTotal = 0.0
         var bolusCount = 0
@@ -103,21 +103,21 @@ class BundleReference {}
         var suspendCount = 0
         var resumeCount = 0
         for event in parsedHistory {
-            switch event {
-            case let .bolus(bolus):
-                bolusTotal += bolus.amount
+            switch event.type {
+            case .bolus:
+                bolusTotal += event.amount.map { ($0 as NSDecimalNumber).doubleValue } ?? 0
                 bolusCount += 1
-                if bolus.isSMB {
+                if event.isSMB == true {
                     smbCount += 1
                 }
-            case let .tempBasal(tempBasal):
-                rateTotal += tempBasal.rate
+            case .tempBasal:
+                rateTotal += event.rate.map { ($0 as NSDecimalNumber).doubleValue } ?? 0
                 tempBasalCount += 1
-            case let .tempBasalDuration(tempBasalDuration):
-                durationTotal += tempBasalDuration.duration
-            case .suspend:
+            case .tempBasalDuration:
+                durationTotal += event.durationMin ?? 0
+            case .pumpSuspend:
                 suspendCount += 1
-            case .resume:
+            case .pumpResume:
                 resumeCount += 1
             default:
                 fatalError("unhandled pump event")
@@ -172,22 +172,15 @@ class BundleReference {}
         ) as? [PumpEventStored] ?? []
 
         let objectIds = allReadings.map(\.objectID)
-        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, orphanedResumes: [], from: context)
+        let parsedHistory = OpenAPS.nativePumpHistory(objectIds, orphanedResumes: [], from: context)
 
         #expect(parsedHistory.count == 1)
 
-        let bolus: BolusDTO? = {
-            switch parsedHistory.first! {
-            case let .bolus(bolus):
-                return bolus
-            default:
-                return nil
-            }
-        }()
-
-        #expect(bolus != nil)
-        #expect(bolus!.isExternal)
-        #expect(bolus!.amount.isApproximatelyEqual(to: 0.88, epsilon: 0.01))
+        let bolus = parsedHistory.first
+        #expect(bolus?.type == .bolus)
+        #expect(bolus?.isExternal == true)
+        let amount = bolus?.amount.map { ($0 as NSDecimalNumber).doubleValue }
+        #expect(amount?.isApproximatelyEqual(to: 0.88, epsilon: 0.01) == true)
     }
 
     @Test("Import carb history with value checks") func testImportCarbHistoryDetails() async throws {

+ 316 - 0
TrioTests/PumpHistoryNativeConversionTests.swift

@@ -0,0 +1,316 @@
+import CoreData
+import Foundation
+import Testing
+
+@testable import Trio
+
+/// Golden tests certifying that the native `PumpEventStored` → `[PumpHistoryEvent]` mapping
+/// (`OpenAPS.nativePumpHistory` / `PumpEventStored.toPumpHistoryEvents`) reproduces, field for
+/// field, the pump history the algorithm used to receive through the old JSON round-trip
+/// (`PumpEventStored` → `[PumpEventDTO]` → JSON → `JSONBridge.pumpHistory`) — with one deliberate
+/// change: a nil `tempType` becomes `temp = nil` instead of throwing during decode.
+@Suite("Pump History Native Conversion Tests", .serialized) struct PumpHistoryNativeConversionTests {
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
+
+    init() async throws {
+        coreDataStack = try await CoreDataStack.createForTests()
+        testContext = coreDataStack.newTaskContext()
+    }
+
+    // MARK: - Golden tests (native mapping vs frozen old-path output)
+
+    @Test("A bolus maps identically") func testBolus() async throws {
+        let ids = [
+            try await insertBolus(id: uuid(1), date: fixedDate(0), amount: 2.5, isSMB: true, isExternal: false)
+        ]
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(
+                id: uuid(1),
+                type: .bolus,
+                timestamp: fixedDate(0),
+                amount: dec("2.5"),
+                duration: 0,
+                isSMB: true,
+                isExternal: false
+            )
+        ])
+    }
+
+    @Test("An external, non-SMB bolus maps identically") func testExternalBolus() async throws {
+        let ids = [
+            try await insertBolus(id: uuid(1), date: fixedDate(0), amount: 0.88, isSMB: false, isExternal: true)
+        ]
+        // 0.88 stored as a Core Data Decimal round-trips through the lossy Double hop to
+        // 0.8800000000000001; the golden freezes that coerced value.
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(
+                id: uuid(1),
+                type: .bolus,
+                timestamp: fixedDate(0),
+                amount: dec("0.8800000000000001"),
+                duration: 0,
+                isSMB: false,
+                isExternal: true
+            )
+        ])
+    }
+
+    @Test("A temp basal emits a duration entry then a rate entry, both identical") func testTempBasal() async throws {
+        let ids = [
+            try await insertTempBasal(id: uuid(1), date: fixedDate(0), rate: 0.85, durationMinutes: 30, tempType: "absolute")
+        ]
+        let native = try await nativeEvents(ids)
+        // Exactly two entries, ordered duration-then-rate.
+        #expect(native.count == 2)
+        #expect(native.first?.type == .tempBasalDuration)
+        #expect(native.first?.durationMin == 30)
+        #expect(native.first?.duration == nil, "the duration entry uses durationMin, never duration")
+        #expect(native.last?.type == .tempBasal)
+        #expect(native.last?.id == "_\(uuid(1))", "the rate entry id is prefixed with an underscore")
+        #expect(native.last?.temp == .absolute)
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(id: uuid(1), type: .tempBasalDuration, timestamp: fixedDate(0), durationMin: 30),
+            PumpHistoryEvent(id: "_\(uuid(1))", type: .tempBasal, timestamp: fixedDate(0), rate: dec("0.85"), temp: .absolute)
+        ])
+    }
+
+    @Test("A percent temp basal maps identically") func testPercentTempBasal() async throws {
+        let ids = [
+            try await insertTempBasal(id: uuid(1), date: fixedDate(0), rate: 1.5, durationMinutes: 45, tempType: "percent")
+        ]
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(id: uuid(1), type: .tempBasalDuration, timestamp: fixedDate(0), durationMin: 45),
+            PumpHistoryEvent(id: "_\(uuid(1))", type: .tempBasal, timestamp: fixedDate(0), rate: dec("1.5"), temp: .percent)
+        ])
+    }
+
+    @Test("A temp basal with a nil rate emits only the duration entry") func testTempBasalNilRate() async throws {
+        let ids = [
+            try await insertTempBasal(id: uuid(1), date: fixedDate(0), rate: nil, durationMinutes: 30, tempType: "absolute")
+        ]
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(id: uuid(1), type: .tempBasalDuration, timestamp: fixedDate(0), durationMin: 30)
+        ])
+    }
+
+    @Test("Suspend, resume, rewind and prime map identically") func testStatusEvents() async throws {
+        let ids = [
+            try await insertStatusEvent(id: uuid(1), date: fixedDate(0), type: .pumpSuspend),
+            try await insertStatusEvent(id: uuid(2), date: fixedDate(1), type: .pumpResume),
+            try await insertStatusEvent(id: uuid(3), date: fixedDate(2), type: .rewind),
+            try await insertStatusEvent(id: uuid(4), date: fixedDate(3), type: .prime)
+        ]
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(id: uuid(1), type: .pumpSuspend, timestamp: fixedDate(0)),
+            PumpHistoryEvent(id: uuid(2), type: .pumpResume, timestamp: fixedDate(1)),
+            PumpHistoryEvent(id: uuid(3), type: .rewind, timestamp: fixedDate(2)),
+            PumpHistoryEvent(id: uuid(4), type: .prime, timestamp: fixedDate(3))
+        ])
+    }
+
+    @Test("A mixed sequence maps identically") func testMixedSequence() async throws {
+        let ids = [
+            try await insertBolus(id: uuid(1), date: fixedDate(0), amount: 1.0, isSMB: true, isExternal: false),
+            try await insertTempBasal(id: uuid(2), date: fixedDate(1), rate: 0.7, durationMinutes: 30, tempType: "absolute"),
+            try await insertStatusEvent(id: uuid(3), date: fixedDate(2), type: .pumpSuspend),
+            try await insertStatusEvent(id: uuid(4), date: fixedDate(3), type: .pumpResume),
+            try await insertBolus(id: uuid(5), date: fixedDate(4), amount: 0.05, isSMB: true, isExternal: false)
+        ]
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(
+                id: uuid(1),
+                type: .bolus,
+                timestamp: fixedDate(0),
+                amount: dec("1.0"),
+                duration: 0,
+                isSMB: true,
+                isExternal: false
+            ),
+            PumpHistoryEvent(id: uuid(2), type: .tempBasalDuration, timestamp: fixedDate(1), durationMin: 30),
+            PumpHistoryEvent(id: "_\(uuid(2))", type: .tempBasal, timestamp: fixedDate(1), rate: dec("0.7"), temp: .absolute),
+            PumpHistoryEvent(id: uuid(3), type: .pumpSuspend, timestamp: fixedDate(2)),
+            PumpHistoryEvent(id: uuid(4), type: .pumpResume, timestamp: fixedDate(3)),
+            PumpHistoryEvent(
+                id: uuid(5),
+                type: .bolus,
+                timestamp: fixedDate(4),
+                amount: dec("0.05"),
+                duration: 0,
+                isSMB: true,
+                isExternal: false
+            )
+        ])
+    }
+
+    @Test("Orphaned resumes are filtered") func testOrphanedResumeFiltering() async throws {
+        let bolus = try await insertBolus(id: uuid(1), date: fixedDate(0), amount: 1.0, isSMB: true, isExternal: false)
+        let orphanedResume = try await insertStatusEvent(id: uuid(2), date: fixedDate(1), type: .pumpResume)
+        let ids = [bolus, orphanedResume]
+
+        try await assertNativeMatchesGolden(ids, orphanedResumes: [orphanedResume], golden: [
+            PumpHistoryEvent(
+                id: uuid(1),
+                type: .bolus,
+                timestamp: fixedDate(0),
+                amount: dec("1.0"),
+                duration: 0,
+                isSMB: true,
+                isExternal: false
+            )
+        ])
+    }
+
+    @Test("Empty history maps to an empty array") func testEmpty() async throws {
+        try await assertNativeMatchesGolden([], golden: [])
+    }
+
+    // MARK: - The one deliberate behavioral change
+
+    @Test("A nil tempType becomes temp = nil instead of throwing") func testNilTempTypeIsRobust() async throws {
+        let ids = [
+            try await insertTempBasal(id: uuid(1), date: fixedDate(0), rate: 0.85, durationMinutes: 30, tempType: nil)
+        ]
+        // The old JSON path emitted the string "unknown" for a nil tempType, which threw while
+        // decoding. Native emits both entries and leaves temp nil instead.
+        try await assertNativeMatchesGolden(ids, golden: [
+            PumpHistoryEvent(id: uuid(1), type: .tempBasalDuration, timestamp: fixedDate(0), durationMin: 30),
+            PumpHistoryEvent(id: "_\(uuid(1))", type: .tempBasal, timestamp: fixedDate(0), rate: dec("0.85"), temp: nil)
+        ])
+    }
+
+    // MARK: - Comparison helpers
+
+    /// Asserts the native mapping reproduces the frozen golden `[PumpHistoryEvent]`.
+    private func assertNativeMatchesGolden(
+        _ objectIDs: [NSManagedObjectID],
+        orphanedResumes: [NSManagedObjectID] = [],
+        golden: [PumpHistoryEvent]
+    ) async throws {
+        let native = try await nativeEvents(objectIDs, orphanedResumes: orphanedResumes)
+
+        #expect(native.count == golden.count, "native produced \(native.count) events, golden has \(golden.count)")
+
+        for (index, pair) in zip(native, golden).enumerated() {
+            expectFieldsEqual(pair.0, pair.1, event: index)
+        }
+    }
+
+    private func nativeEvents(
+        _ objectIDs: [NSManagedObjectID],
+        orphanedResumes: [NSManagedObjectID] = []
+    ) async throws -> [PumpHistoryEvent] {
+        try await testContext.perform {
+            OpenAPS.nativePumpHistory(objectIDs, orphanedResumes: orphanedResumes, from: self.testContext)
+        }
+    }
+
+    /// Field-by-field comparison, comparing `timestamp` at millisecond resolution.
+    private func expectFieldsEqual(_ actual: PumpHistoryEvent, _ expected: PumpHistoryEvent, event index: Int) {
+        #expect(actual.id == expected.id, "event \(index): id \(actual.id) != \(expected.id)")
+        #expect(actual.type == expected.type, "event \(index): type \(actual.type) != \(expected.type)")
+        #expect(
+            Self.millisecondString(actual.timestamp) == Self.millisecondString(expected.timestamp),
+            "event \(index): timestamp mismatch"
+        )
+        #expect(actual.amount == expected.amount, "event \(index): amount \(desc(actual.amount)) != \(desc(expected.amount))")
+        #expect(
+            actual.duration == expected.duration,
+            "event \(index): duration \(desc(actual.duration)) != \(desc(expected.duration))"
+        )
+        #expect(
+            actual.durationMin == expected.durationMin,
+            "event \(index): durationMin \(desc(actual.durationMin)) != \(desc(expected.durationMin))"
+        )
+        #expect(actual.rate == expected.rate, "event \(index): rate \(desc(actual.rate)) != \(desc(expected.rate))")
+        #expect(actual.temp == expected.temp, "event \(index): temp \(desc(actual.temp)) != \(desc(expected.temp))")
+        #expect(actual.isSMB == expected.isSMB, "event \(index): isSMB \(desc(actual.isSMB)) != \(desc(expected.isSMB))")
+        #expect(
+            actual.isExternal == expected.isExternal,
+            "event \(index): isExternal \(desc(actual.isExternal)) != \(desc(expected.isExternal))"
+        )
+    }
+
+    private func desc<T>(_ value: T?) -> String { value.map { "\($0)" } ?? "nil" }
+
+    /// Builds a `Decimal` from its string form, matching `Decimal(algorithmValue:)`.
+    private func dec(_ string: String) -> Decimal { Decimal(string: string) ?? .zero }
+
+    private static func millisecondString(_ date: Date) -> String {
+        Formatter.iso8601withFractionalSeconds.string(from: date)
+    }
+
+    // MARK: - Fixture helpers
+
+    private func insertBolus(
+        id: String,
+        date: Date,
+        amount: Double,
+        isSMB: Bool,
+        isExternal: Bool
+    ) async throws -> NSManagedObjectID {
+        try await testContext.perform {
+            let event = PumpEventStored(context: self.testContext)
+            event.id = id
+            event.timestamp = date
+            event.type = PumpEventStored.EventType.bolus.rawValue
+
+            let bolus = BolusStored(context: self.testContext)
+            bolus.amount = NSDecimalNumber(value: amount)
+            bolus.isSMB = isSMB
+            bolus.isExternal = isExternal
+            bolus.pumpEvent = event
+
+            try self.testContext.save()
+            return event.objectID
+        }
+    }
+
+    private func insertTempBasal(
+        id: String,
+        date: Date,
+        rate: Double?,
+        durationMinutes: Int,
+        tempType: String?
+    ) async throws -> NSManagedObjectID {
+        try await testContext.perform {
+            let event = PumpEventStored(context: self.testContext)
+            event.id = id
+            event.timestamp = date
+            event.type = PumpEventStored.EventType.tempBasal.rawValue
+
+            let tempBasal = TempBasalStored(context: self.testContext)
+            tempBasal.rate = rate.map { NSDecimalNumber(value: $0) }
+            tempBasal.duration = Int16(durationMinutes)
+            tempBasal.tempType = tempType
+            tempBasal.pumpEvent = event
+
+            try self.testContext.save()
+            return event.objectID
+        }
+    }
+
+    private func insertStatusEvent(
+        id: String,
+        date: Date,
+        type: PumpEventStored.EventType
+    ) async throws -> NSManagedObjectID {
+        try await testContext.perform {
+            let event = PumpEventStored(context: self.testContext)
+            event.id = id
+            event.timestamp = date
+            event.type = type.rawValue
+            try self.testContext.save()
+            return event.objectID
+        }
+    }
+
+    /// A fixed base timestamp on whole seconds so fixtures are deterministic.
+    private func fixedDate(_ minutesAgo: Double) -> Date {
+        Date(timeIntervalSince1970: 1_700_000_000 - minutesAgo * 60)
+    }
+
+    private func uuid(_ n: Int) -> String {
+        String(format: "00000000-0000-0000-0000-%012d", n)
+    }
+}